plugin.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  1. /**
  2. * TinyMCE version 6.0.3 (2022-05-25)
  3. */
  4. (function () {
  5. 'use strict';
  6. var global$5 = tinymce.util.Tools.resolve('tinymce.PluginManager');
  7. var global$4 = tinymce.util.Tools.resolve('tinymce.util.VK');
  8. const hasProto = (v, constructor, predicate) => {
  9. var _a;
  10. if (predicate(v, constructor.prototype)) {
  11. return true;
  12. } else {
  13. return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name;
  14. }
  15. };
  16. const typeOf = x => {
  17. const t = typeof x;
  18. if (x === null) {
  19. return 'null';
  20. } else if (t === 'object' && Array.isArray(x)) {
  21. return 'array';
  22. } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
  23. return 'string';
  24. } else {
  25. return t;
  26. }
  27. };
  28. const isType = type => value => typeOf(value) === type;
  29. const isSimpleType = type => value => typeof value === type;
  30. const eq = t => a => t === a;
  31. const isString = isType('string');
  32. const isObject = isType('object');
  33. const isArray = isType('array');
  34. const isNull = eq(null);
  35. const isBoolean = isSimpleType('boolean');
  36. const isNullable = a => a === null || a === undefined;
  37. const isNonNullable = a => !isNullable(a);
  38. const isFunction = isSimpleType('function');
  39. const isArrayOf = (value, pred) => {
  40. if (isArray(value)) {
  41. for (let i = 0, len = value.length; i < len; ++i) {
  42. if (!pred(value[i])) {
  43. return false;
  44. }
  45. }
  46. return true;
  47. }
  48. return false;
  49. };
  50. const noop = () => {
  51. };
  52. const tripleEquals = (a, b) => {
  53. return a === b;
  54. };
  55. class Optional {
  56. constructor(tag, value) {
  57. this.tag = tag;
  58. this.value = value;
  59. }
  60. static some(value) {
  61. return new Optional(true, value);
  62. }
  63. static none() {
  64. return Optional.singletonNone;
  65. }
  66. fold(onNone, onSome) {
  67. if (this.tag) {
  68. return onSome(this.value);
  69. } else {
  70. return onNone();
  71. }
  72. }
  73. isSome() {
  74. return this.tag;
  75. }
  76. isNone() {
  77. return !this.tag;
  78. }
  79. map(mapper) {
  80. if (this.tag) {
  81. return Optional.some(mapper(this.value));
  82. } else {
  83. return Optional.none();
  84. }
  85. }
  86. bind(binder) {
  87. if (this.tag) {
  88. return binder(this.value);
  89. } else {
  90. return Optional.none();
  91. }
  92. }
  93. exists(predicate) {
  94. return this.tag && predicate(this.value);
  95. }
  96. forall(predicate) {
  97. return !this.tag || predicate(this.value);
  98. }
  99. filter(predicate) {
  100. if (!this.tag || predicate(this.value)) {
  101. return this;
  102. } else {
  103. return Optional.none();
  104. }
  105. }
  106. getOr(replacement) {
  107. return this.tag ? this.value : replacement;
  108. }
  109. or(replacement) {
  110. return this.tag ? this : replacement;
  111. }
  112. getOrThunk(thunk) {
  113. return this.tag ? this.value : thunk();
  114. }
  115. orThunk(thunk) {
  116. return this.tag ? this : thunk();
  117. }
  118. getOrDie(message) {
  119. if (!this.tag) {
  120. throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None');
  121. } else {
  122. return this.value;
  123. }
  124. }
  125. static from(value) {
  126. return isNonNullable(value) ? Optional.some(value) : Optional.none();
  127. }
  128. getOrNull() {
  129. return this.tag ? this.value : null;
  130. }
  131. getOrUndefined() {
  132. return this.value;
  133. }
  134. each(worker) {
  135. if (this.tag) {
  136. worker(this.value);
  137. }
  138. }
  139. toArray() {
  140. return this.tag ? [this.value] : [];
  141. }
  142. toString() {
  143. return this.tag ? `some(${ this.value })` : 'none()';
  144. }
  145. }
  146. Optional.singletonNone = new Optional(false);
  147. const nativeIndexOf = Array.prototype.indexOf;
  148. const nativePush = Array.prototype.push;
  149. const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t);
  150. const contains = (xs, x) => rawIndexOf(xs, x) > -1;
  151. const map = (xs, f) => {
  152. const len = xs.length;
  153. const r = new Array(len);
  154. for (let i = 0; i < len; i++) {
  155. const x = xs[i];
  156. r[i] = f(x, i);
  157. }
  158. return r;
  159. };
  160. const each$1 = (xs, f) => {
  161. for (let i = 0, len = xs.length; i < len; i++) {
  162. const x = xs[i];
  163. f(x, i);
  164. }
  165. };
  166. const foldl = (xs, f, acc) => {
  167. each$1(xs, (x, i) => {
  168. acc = f(acc, x, i);
  169. });
  170. return acc;
  171. };
  172. const flatten = xs => {
  173. const r = [];
  174. for (let i = 0, len = xs.length; i < len; ++i) {
  175. if (!isArray(xs[i])) {
  176. throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
  177. }
  178. nativePush.apply(r, xs[i]);
  179. }
  180. return r;
  181. };
  182. const bind = (xs, f) => flatten(map(xs, f));
  183. const findMap = (arr, f) => {
  184. for (let i = 0; i < arr.length; i++) {
  185. const r = f(arr[i], i);
  186. if (r.isSome()) {
  187. return r;
  188. }
  189. }
  190. return Optional.none();
  191. };
  192. const is = (lhs, rhs, comparator = tripleEquals) => lhs.exists(left => comparator(left, rhs));
  193. const cat = arr => {
  194. const r = [];
  195. const push = x => {
  196. r.push(x);
  197. };
  198. for (let i = 0; i < arr.length; i++) {
  199. arr[i].each(push);
  200. }
  201. return r;
  202. };
  203. const someIf = (b, a) => b ? Optional.some(a) : Optional.none();
  204. const option = name => editor => editor.options.get(name);
  205. const register$1 = editor => {
  206. const registerOption = editor.options.register;
  207. registerOption('link_assume_external_targets', {
  208. processor: value => {
  209. const valid = isString(value) || isBoolean(value);
  210. if (valid) {
  211. if (value === true) {
  212. return {
  213. value: 1,
  214. valid
  215. };
  216. } else if (value === 'http' || value === 'https') {
  217. return {
  218. value,
  219. valid
  220. };
  221. } else {
  222. return {
  223. value: 0,
  224. valid
  225. };
  226. }
  227. } else {
  228. return {
  229. valid: false,
  230. message: 'Must be a string or a boolean.'
  231. };
  232. }
  233. },
  234. default: false
  235. });
  236. registerOption('link_context_toolbar', {
  237. processor: 'boolean',
  238. default: false
  239. });
  240. registerOption('link_list', { processor: value => isString(value) || isFunction(value) || isArrayOf(value, isObject) });
  241. registerOption('link_default_target', { processor: 'string' });
  242. registerOption('link_default_protocol', {
  243. processor: 'string',
  244. default: 'https'
  245. });
  246. registerOption('link_target_list', {
  247. processor: value => isBoolean(value) || isArrayOf(value, isObject),
  248. default: true
  249. });
  250. registerOption('link_rel_list', {
  251. processor: 'object[]',
  252. default: []
  253. });
  254. registerOption('link_class_list', {
  255. processor: 'object[]',
  256. default: []
  257. });
  258. registerOption('link_title', {
  259. processor: 'boolean',
  260. default: true
  261. });
  262. registerOption('allow_unsafe_link_target', {
  263. processor: 'boolean',
  264. default: false
  265. });
  266. registerOption('link_quicklink', {
  267. processor: 'boolean',
  268. default: false
  269. });
  270. };
  271. const assumeExternalTargets = option('link_assume_external_targets');
  272. const hasContextToolbar = option('link_context_toolbar');
  273. const getLinkList = option('link_list');
  274. const getDefaultLinkTarget = option('link_default_target');
  275. const getDefaultLinkProtocol = option('link_default_protocol');
  276. const getTargetList = option('link_target_list');
  277. const getRelList = option('link_rel_list');
  278. const getLinkClassList = option('link_class_list');
  279. const shouldShowLinkTitle = option('link_title');
  280. const allowUnsafeLinkTarget = option('allow_unsafe_link_target');
  281. const useQuickLink = option('link_quicklink');
  282. var global$3 = tinymce.util.Tools.resolve('tinymce.util.Tools');
  283. const getValue = item => isString(item.value) ? item.value : '';
  284. const getText = item => {
  285. if (isString(item.text)) {
  286. return item.text;
  287. } else if (isString(item.title)) {
  288. return item.title;
  289. } else {
  290. return '';
  291. }
  292. };
  293. const sanitizeList = (list, extractValue) => {
  294. const out = [];
  295. global$3.each(list, item => {
  296. const text = getText(item);
  297. if (item.menu !== undefined) {
  298. const items = sanitizeList(item.menu, extractValue);
  299. out.push({
  300. text,
  301. items
  302. });
  303. } else {
  304. const value = extractValue(item);
  305. out.push({
  306. text,
  307. value
  308. });
  309. }
  310. });
  311. return out;
  312. };
  313. const sanitizeWith = (extracter = getValue) => list => Optional.from(list).map(list => sanitizeList(list, extracter));
  314. const sanitize = list => sanitizeWith(getValue)(list);
  315. const createUi = (name, label) => items => ({
  316. name,
  317. type: 'listbox',
  318. label,
  319. items
  320. });
  321. const ListOptions = {
  322. sanitize,
  323. sanitizeWith,
  324. createUi,
  325. getValue
  326. };
  327. const keys = Object.keys;
  328. const hasOwnProperty = Object.hasOwnProperty;
  329. const each = (obj, f) => {
  330. const props = keys(obj);
  331. for (let k = 0, len = props.length; k < len; k++) {
  332. const i = props[k];
  333. const x = obj[i];
  334. f(x, i);
  335. }
  336. };
  337. const objAcc = r => (x, i) => {
  338. r[i] = x;
  339. };
  340. const internalFilter = (obj, pred, onTrue, onFalse) => {
  341. const r = {};
  342. each(obj, (x, i) => {
  343. (pred(x, i) ? onTrue : onFalse)(x, i);
  344. });
  345. return r;
  346. };
  347. const filter = (obj, pred) => {
  348. const t = {};
  349. internalFilter(obj, pred, objAcc(t), noop);
  350. return t;
  351. };
  352. const has = (obj, key) => hasOwnProperty.call(obj, key);
  353. const hasNonNullableKey = (obj, key) => has(obj, key) && obj[key] !== undefined && obj[key] !== null;
  354. var global$2 = tinymce.util.Tools.resolve('tinymce.dom.TreeWalker');
  355. var global$1 = tinymce.util.Tools.resolve('tinymce.util.URI');
  356. const isAnchor = elm => elm && elm.nodeName.toLowerCase() === 'a';
  357. const isLink = elm => isAnchor(elm) && !!getHref(elm);
  358. const collectNodesInRange = (rng, predicate) => {
  359. if (rng.collapsed) {
  360. return [];
  361. } else {
  362. const contents = rng.cloneContents();
  363. const walker = new global$2(contents.firstChild, contents);
  364. const elements = [];
  365. let current = contents.firstChild;
  366. do {
  367. if (predicate(current)) {
  368. elements.push(current);
  369. }
  370. } while (current = walker.next());
  371. return elements;
  372. }
  373. };
  374. const hasProtocol = url => /^\w+:/i.test(url);
  375. const getHref = elm => {
  376. const href = elm.getAttribute('data-mce-href');
  377. return href ? href : elm.getAttribute('href');
  378. };
  379. const applyRelTargetRules = (rel, isUnsafe) => {
  380. const rules = ['noopener'];
  381. const rels = rel ? rel.split(/\s+/) : [];
  382. const toString = rels => global$3.trim(rels.sort().join(' '));
  383. const addTargetRules = rels => {
  384. rels = removeTargetRules(rels);
  385. return rels.length > 0 ? rels.concat(rules) : rules;
  386. };
  387. const removeTargetRules = rels => rels.filter(val => global$3.inArray(rules, val) === -1);
  388. const newRels = isUnsafe ? addTargetRules(rels) : removeTargetRules(rels);
  389. return newRels.length > 0 ? toString(newRels) : '';
  390. };
  391. const trimCaretContainers = text => text.replace(/\uFEFF/g, '');
  392. const getAnchorElement = (editor, selectedElm) => {
  393. selectedElm = selectedElm || editor.selection.getNode();
  394. if (isImageFigure(selectedElm)) {
  395. return editor.dom.select('a[href]', selectedElm)[0];
  396. } else {
  397. return editor.dom.getParent(selectedElm, 'a[href]');
  398. }
  399. };
  400. const getAnchorText = (selection, anchorElm) => {
  401. const text = anchorElm ? anchorElm.innerText || anchorElm.textContent : selection.getContent({ format: 'text' });
  402. return trimCaretContainers(text);
  403. };
  404. const hasLinks = elements => global$3.grep(elements, isLink).length > 0;
  405. const hasLinksInSelection = rng => collectNodesInRange(rng, isLink).length > 0;
  406. const isOnlyTextSelected = editor => {
  407. const inlineTextElements = editor.schema.getTextInlineElements();
  408. const isElement = elm => elm.nodeType === 1 && !isAnchor(elm) && !has(inlineTextElements, elm.nodeName.toLowerCase());
  409. const elements = collectNodesInRange(editor.selection.getRng(), isElement);
  410. return elements.length === 0;
  411. };
  412. const isImageFigure = elm => elm && elm.nodeName === 'FIGURE' && /\bimage\b/i.test(elm.className);
  413. const getLinkAttrs = data => {
  414. const attrs = [
  415. 'title',
  416. 'rel',
  417. 'class',
  418. 'target'
  419. ];
  420. return foldl(attrs, (acc, key) => {
  421. data[key].each(value => {
  422. acc[key] = value.length > 0 ? value : null;
  423. });
  424. return acc;
  425. }, { href: data.href });
  426. };
  427. const handleExternalTargets = (href, assumeExternalTargets) => {
  428. if ((assumeExternalTargets === 'http' || assumeExternalTargets === 'https') && !hasProtocol(href)) {
  429. return assumeExternalTargets + '://' + href;
  430. }
  431. return href;
  432. };
  433. const applyLinkOverrides = (editor, linkAttrs) => {
  434. const newLinkAttrs = { ...linkAttrs };
  435. if (getRelList(editor).length === 0 && !allowUnsafeLinkTarget(editor)) {
  436. const newRel = applyRelTargetRules(newLinkAttrs.rel, newLinkAttrs.target === '_blank');
  437. newLinkAttrs.rel = newRel ? newRel : null;
  438. }
  439. if (Optional.from(newLinkAttrs.target).isNone() && getTargetList(editor) === false) {
  440. newLinkAttrs.target = getDefaultLinkTarget(editor);
  441. }
  442. newLinkAttrs.href = handleExternalTargets(newLinkAttrs.href, assumeExternalTargets(editor));
  443. return newLinkAttrs;
  444. };
  445. const updateLink = (editor, anchorElm, text, linkAttrs) => {
  446. text.each(text => {
  447. if (has(anchorElm, 'innerText')) {
  448. anchorElm.innerText = text;
  449. } else {
  450. anchorElm.textContent = text;
  451. }
  452. });
  453. editor.dom.setAttribs(anchorElm, linkAttrs);
  454. editor.selection.select(anchorElm);
  455. };
  456. const createLink = (editor, selectedElm, text, linkAttrs) => {
  457. if (isImageFigure(selectedElm)) {
  458. linkImageFigure(editor, selectedElm, linkAttrs);
  459. } else {
  460. text.fold(() => {
  461. editor.execCommand('mceInsertLink', false, linkAttrs);
  462. }, text => {
  463. editor.insertContent(editor.dom.createHTML('a', linkAttrs, editor.dom.encode(text)));
  464. });
  465. }
  466. };
  467. const linkDomMutation = (editor, attachState, data) => {
  468. const selectedElm = editor.selection.getNode();
  469. const anchorElm = getAnchorElement(editor, selectedElm);
  470. const linkAttrs = applyLinkOverrides(editor, getLinkAttrs(data));
  471. editor.undoManager.transact(() => {
  472. if (data.href === attachState.href) {
  473. attachState.attach();
  474. }
  475. if (anchorElm) {
  476. editor.focus();
  477. updateLink(editor, anchorElm, data.text, linkAttrs);
  478. } else {
  479. createLink(editor, selectedElm, data.text, linkAttrs);
  480. }
  481. });
  482. };
  483. const unlinkSelection = editor => {
  484. const dom = editor.dom, selection = editor.selection;
  485. const bookmark = selection.getBookmark();
  486. const rng = selection.getRng().cloneRange();
  487. const startAnchorElm = dom.getParent(rng.startContainer, 'a[href]', editor.getBody());
  488. const endAnchorElm = dom.getParent(rng.endContainer, 'a[href]', editor.getBody());
  489. if (startAnchorElm) {
  490. rng.setStartBefore(startAnchorElm);
  491. }
  492. if (endAnchorElm) {
  493. rng.setEndAfter(endAnchorElm);
  494. }
  495. selection.setRng(rng);
  496. editor.execCommand('unlink');
  497. selection.moveToBookmark(bookmark);
  498. };
  499. const unlinkDomMutation = editor => {
  500. editor.undoManager.transact(() => {
  501. const node = editor.selection.getNode();
  502. if (isImageFigure(node)) {
  503. unlinkImageFigure(editor, node);
  504. } else {
  505. unlinkSelection(editor);
  506. }
  507. editor.focus();
  508. });
  509. };
  510. const unwrapOptions = data => {
  511. const {
  512. class: cls,
  513. href,
  514. rel,
  515. target,
  516. text,
  517. title
  518. } = data;
  519. return filter({
  520. class: cls.getOrNull(),
  521. href,
  522. rel: rel.getOrNull(),
  523. target: target.getOrNull(),
  524. text: text.getOrNull(),
  525. title: title.getOrNull()
  526. }, (v, _k) => isNull(v) === false);
  527. };
  528. const sanitizeData = (editor, data) => {
  529. const getOption = editor.options.get;
  530. const uriOptions = {
  531. allow_html_data_urls: getOption('allow_html_data_urls'),
  532. allow_script_urls: getOption('allow_script_urls'),
  533. allow_svg_data_urls: getOption('allow_svg_data_urls')
  534. };
  535. const href = data.href;
  536. return {
  537. ...data,
  538. href: global$1.isDomSafe(href, 'a', uriOptions) ? href : ''
  539. };
  540. };
  541. const link = (editor, attachState, data) => {
  542. const sanitizedData = sanitizeData(editor, data);
  543. editor.hasPlugin('rtc', true) ? editor.execCommand('createlink', false, unwrapOptions(sanitizedData)) : linkDomMutation(editor, attachState, sanitizedData);
  544. };
  545. const unlink = editor => {
  546. editor.hasPlugin('rtc', true) ? editor.execCommand('unlink') : unlinkDomMutation(editor);
  547. };
  548. const unlinkImageFigure = (editor, fig) => {
  549. const img = editor.dom.select('img', fig)[0];
  550. if (img) {
  551. const a = editor.dom.getParents(img, 'a[href]', fig)[0];
  552. if (a) {
  553. a.parentNode.insertBefore(img, a);
  554. editor.dom.remove(a);
  555. }
  556. }
  557. };
  558. const linkImageFigure = (editor, fig, attrs) => {
  559. const img = editor.dom.select('img', fig)[0];
  560. if (img) {
  561. const a = editor.dom.create('a', attrs);
  562. img.parentNode.insertBefore(a, img);
  563. a.appendChild(img);
  564. }
  565. };
  566. const isListGroup = item => hasNonNullableKey(item, 'items');
  567. const findTextByValue = (value, catalog) => findMap(catalog, item => {
  568. if (isListGroup(item)) {
  569. return findTextByValue(value, item.items);
  570. } else {
  571. return someIf(item.value === value, item);
  572. }
  573. });
  574. const getDelta = (persistentText, fieldName, catalog, data) => {
  575. const value = data[fieldName];
  576. const hasPersistentText = persistentText.length > 0;
  577. return value !== undefined ? findTextByValue(value, catalog).map(i => ({
  578. url: {
  579. value: i.value,
  580. meta: {
  581. text: hasPersistentText ? persistentText : i.text,
  582. attach: noop
  583. }
  584. },
  585. text: hasPersistentText ? persistentText : i.text
  586. })) : Optional.none();
  587. };
  588. const findCatalog = (catalogs, fieldName) => {
  589. if (fieldName === 'link') {
  590. return catalogs.link;
  591. } else if (fieldName === 'anchor') {
  592. return catalogs.anchor;
  593. } else {
  594. return Optional.none();
  595. }
  596. };
  597. const init = (initialData, linkCatalog) => {
  598. const persistentData = {
  599. text: initialData.text,
  600. title: initialData.title
  601. };
  602. const getTitleFromUrlChange = url => someIf(persistentData.title.length <= 0, Optional.from(url.meta.title).getOr(''));
  603. const getTextFromUrlChange = url => someIf(persistentData.text.length <= 0, Optional.from(url.meta.text).getOr(url.value));
  604. const onUrlChange = data => {
  605. const text = getTextFromUrlChange(data.url);
  606. const title = getTitleFromUrlChange(data.url);
  607. if (text.isSome() || title.isSome()) {
  608. return Optional.some({
  609. ...text.map(text => ({ text })).getOr({}),
  610. ...title.map(title => ({ title })).getOr({})
  611. });
  612. } else {
  613. return Optional.none();
  614. }
  615. };
  616. const onCatalogChange = (data, change) => {
  617. const catalog = findCatalog(linkCatalog, change.name).getOr([]);
  618. return getDelta(persistentData.text, change.name, catalog, data);
  619. };
  620. const onChange = (getData, change) => {
  621. const name = change.name;
  622. if (name === 'url') {
  623. return onUrlChange(getData());
  624. } else if (contains([
  625. 'anchor',
  626. 'link'
  627. ], name)) {
  628. return onCatalogChange(getData(), change);
  629. } else if (name === 'text' || name === 'title') {
  630. persistentData[name] = getData()[name];
  631. return Optional.none();
  632. } else {
  633. return Optional.none();
  634. }
  635. };
  636. return { onChange };
  637. };
  638. const DialogChanges = {
  639. init,
  640. getDelta
  641. };
  642. var global = tinymce.util.Tools.resolve('tinymce.util.Delay');
  643. const delayedConfirm = (editor, message, callback) => {
  644. const rng = editor.selection.getRng();
  645. global.setEditorTimeout(editor, () => {
  646. editor.windowManager.confirm(message, state => {
  647. editor.selection.setRng(rng);
  648. callback(state);
  649. });
  650. });
  651. };
  652. const tryEmailTransform = data => {
  653. const url = data.href;
  654. const suggestMailTo = url.indexOf('@') > 0 && url.indexOf('/') === -1 && url.indexOf('mailto:') === -1;
  655. return suggestMailTo ? Optional.some({
  656. message: 'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?',
  657. preprocess: oldData => ({
  658. ...oldData,
  659. href: 'mailto:' + url
  660. })
  661. }) : Optional.none();
  662. };
  663. const tryProtocolTransform = (assumeExternalTargets, defaultLinkProtocol) => data => {
  664. const url = data.href;
  665. const suggestProtocol = assumeExternalTargets === 1 && !hasProtocol(url) || assumeExternalTargets === 0 && /^\s*www(\.|\d\.)/i.test(url);
  666. return suggestProtocol ? Optional.some({
  667. message: `The URL you entered seems to be an external link. Do you want to add the required ${ defaultLinkProtocol }:// prefix?`,
  668. preprocess: oldData => ({
  669. ...oldData,
  670. href: defaultLinkProtocol + '://' + url
  671. })
  672. }) : Optional.none();
  673. };
  674. const preprocess = (editor, data) => findMap([
  675. tryEmailTransform,
  676. tryProtocolTransform(assumeExternalTargets(editor), getDefaultLinkProtocol(editor))
  677. ], f => f(data)).fold(() => Promise.resolve(data), transform => new Promise(callback => {
  678. delayedConfirm(editor, transform.message, state => {
  679. callback(state ? transform.preprocess(data) : data);
  680. });
  681. }));
  682. const DialogConfirms = { preprocess };
  683. const getAnchors = editor => {
  684. const anchorNodes = editor.dom.select('a:not([href])');
  685. const anchors = bind(anchorNodes, anchor => {
  686. const id = anchor.name || anchor.id;
  687. return id ? [{
  688. text: id,
  689. value: '#' + id
  690. }] : [];
  691. });
  692. return anchors.length > 0 ? Optional.some([{
  693. text: 'None',
  694. value: ''
  695. }].concat(anchors)) : Optional.none();
  696. };
  697. const AnchorListOptions = { getAnchors };
  698. const getClasses = editor => {
  699. const list = getLinkClassList(editor);
  700. if (list.length > 0) {
  701. return ListOptions.sanitize(list);
  702. }
  703. return Optional.none();
  704. };
  705. const ClassListOptions = { getClasses };
  706. const parseJson = text => {
  707. try {
  708. return Optional.some(JSON.parse(text));
  709. } catch (err) {
  710. return Optional.none();
  711. }
  712. };
  713. const getLinks = editor => {
  714. const extractor = item => editor.convertURL(item.value || item.url, 'href');
  715. const linkList = getLinkList(editor);
  716. return new Promise(resolve => {
  717. if (isString(linkList)) {
  718. fetch(linkList).then(res => res.ok ? res.text().then(parseJson) : Promise.reject()).then(resolve, () => resolve(Optional.none()));
  719. } else if (isFunction(linkList)) {
  720. linkList(output => resolve(Optional.some(output)));
  721. } else {
  722. resolve(Optional.from(linkList));
  723. }
  724. }).then(optItems => optItems.bind(ListOptions.sanitizeWith(extractor)).map(items => {
  725. if (items.length > 0) {
  726. const noneItem = [{
  727. text: 'None',
  728. value: ''
  729. }];
  730. return noneItem.concat(items);
  731. } else {
  732. return items;
  733. }
  734. }));
  735. };
  736. const LinkListOptions = { getLinks };
  737. const getRels = (editor, initialTarget) => {
  738. const list = getRelList(editor);
  739. if (list.length > 0) {
  740. const isTargetBlank = is(initialTarget, '_blank');
  741. const enforceSafe = allowUnsafeLinkTarget(editor) === false;
  742. const safeRelExtractor = item => applyRelTargetRules(ListOptions.getValue(item), isTargetBlank);
  743. const sanitizer = enforceSafe ? ListOptions.sanitizeWith(safeRelExtractor) : ListOptions.sanitize;
  744. return sanitizer(list);
  745. }
  746. return Optional.none();
  747. };
  748. const RelOptions = { getRels };
  749. const fallbacks = [
  750. {
  751. text: 'Current window',
  752. value: ''
  753. },
  754. {
  755. text: 'New window',
  756. value: '_blank'
  757. }
  758. ];
  759. const getTargets = editor => {
  760. const list = getTargetList(editor);
  761. if (isArray(list)) {
  762. return ListOptions.sanitize(list).orThunk(() => Optional.some(fallbacks));
  763. } else if (list === false) {
  764. return Optional.none();
  765. }
  766. return Optional.some(fallbacks);
  767. };
  768. const TargetOptions = { getTargets };
  769. const nonEmptyAttr = (dom, elem, name) => {
  770. const val = dom.getAttrib(elem, name);
  771. return val !== null && val.length > 0 ? Optional.some(val) : Optional.none();
  772. };
  773. const extractFromAnchor = (editor, anchor) => {
  774. const dom = editor.dom;
  775. const onlyText = isOnlyTextSelected(editor);
  776. const text = onlyText ? Optional.some(getAnchorText(editor.selection, anchor)) : Optional.none();
  777. const url = anchor ? Optional.some(dom.getAttrib(anchor, 'href')) : Optional.none();
  778. const target = anchor ? Optional.from(dom.getAttrib(anchor, 'target')) : Optional.none();
  779. const rel = nonEmptyAttr(dom, anchor, 'rel');
  780. const linkClass = nonEmptyAttr(dom, anchor, 'class');
  781. const title = nonEmptyAttr(dom, anchor, 'title');
  782. return {
  783. url,
  784. text,
  785. title,
  786. target,
  787. rel,
  788. linkClass
  789. };
  790. };
  791. const collect = (editor, linkNode) => LinkListOptions.getLinks(editor).then(links => {
  792. const anchor = extractFromAnchor(editor, linkNode);
  793. return {
  794. anchor,
  795. catalogs: {
  796. targets: TargetOptions.getTargets(editor),
  797. rels: RelOptions.getRels(editor, anchor.target),
  798. classes: ClassListOptions.getClasses(editor),
  799. anchor: AnchorListOptions.getAnchors(editor),
  800. link: links
  801. },
  802. optNode: Optional.from(linkNode),
  803. flags: { titleEnabled: shouldShowLinkTitle(editor) }
  804. };
  805. });
  806. const DialogInfo = { collect };
  807. const handleSubmit = (editor, info) => api => {
  808. const data = api.getData();
  809. if (!data.url.value) {
  810. unlink(editor);
  811. api.close();
  812. return;
  813. }
  814. const getChangedValue = key => Optional.from(data[key]).filter(value => !is(info.anchor[key], value));
  815. const changedData = {
  816. href: data.url.value,
  817. text: getChangedValue('text'),
  818. target: getChangedValue('target'),
  819. rel: getChangedValue('rel'),
  820. class: getChangedValue('linkClass'),
  821. title: getChangedValue('title')
  822. };
  823. const attachState = {
  824. href: data.url.value,
  825. attach: data.url.meta !== undefined && data.url.meta.attach ? data.url.meta.attach : noop
  826. };
  827. DialogConfirms.preprocess(editor, changedData).then(pData => {
  828. link(editor, attachState, pData);
  829. });
  830. api.close();
  831. };
  832. const collectData = editor => {
  833. const anchorNode = getAnchorElement(editor);
  834. return DialogInfo.collect(editor, anchorNode);
  835. };
  836. const getInitialData = (info, defaultTarget) => {
  837. const anchor = info.anchor;
  838. const url = anchor.url.getOr('');
  839. return {
  840. url: {
  841. value: url,
  842. meta: { original: { value: url } }
  843. },
  844. text: anchor.text.getOr(''),
  845. title: anchor.title.getOr(''),
  846. anchor: url,
  847. link: url,
  848. rel: anchor.rel.getOr(''),
  849. target: anchor.target.or(defaultTarget).getOr(''),
  850. linkClass: anchor.linkClass.getOr('')
  851. };
  852. };
  853. const makeDialog = (settings, onSubmit, editor) => {
  854. const urlInput = [{
  855. name: 'url',
  856. type: 'urlinput',
  857. filetype: 'file',
  858. label: 'URL'
  859. }];
  860. const displayText = settings.anchor.text.map(() => ({
  861. name: 'text',
  862. type: 'input',
  863. label: 'Text to display'
  864. })).toArray();
  865. const titleText = settings.flags.titleEnabled ? [{
  866. name: 'title',
  867. type: 'input',
  868. label: 'Title'
  869. }] : [];
  870. const defaultTarget = Optional.from(getDefaultLinkTarget(editor));
  871. const initialData = getInitialData(settings, defaultTarget);
  872. const catalogs = settings.catalogs;
  873. const dialogDelta = DialogChanges.init(initialData, catalogs);
  874. const body = {
  875. type: 'panel',
  876. items: flatten([
  877. urlInput,
  878. displayText,
  879. titleText,
  880. cat([
  881. catalogs.anchor.map(ListOptions.createUi('anchor', 'Anchors')),
  882. catalogs.rels.map(ListOptions.createUi('rel', 'Rel')),
  883. catalogs.targets.map(ListOptions.createUi('target', 'Open link in...')),
  884. catalogs.link.map(ListOptions.createUi('link', 'Link list')),
  885. catalogs.classes.map(ListOptions.createUi('linkClass', 'Class'))
  886. ])
  887. ])
  888. };
  889. return {
  890. title: 'Insert/Edit Link',
  891. size: 'normal',
  892. body,
  893. buttons: [
  894. {
  895. type: 'cancel',
  896. name: 'cancel',
  897. text: 'Cancel'
  898. },
  899. {
  900. type: 'submit',
  901. name: 'save',
  902. text: 'Save',
  903. primary: true
  904. }
  905. ],
  906. initialData,
  907. onChange: (api, {name}) => {
  908. dialogDelta.onChange(api.getData, { name }).each(newData => {
  909. api.setData(newData);
  910. });
  911. },
  912. onSubmit
  913. };
  914. };
  915. const open$1 = editor => {
  916. const data = collectData(editor);
  917. data.then(info => {
  918. const onSubmit = handleSubmit(editor, info);
  919. return makeDialog(info, onSubmit, editor);
  920. }).then(spec => {
  921. editor.windowManager.open(spec);
  922. });
  923. };
  924. const appendClickRemove = (link, evt) => {
  925. document.body.appendChild(link);
  926. link.dispatchEvent(evt);
  927. document.body.removeChild(link);
  928. };
  929. const open = url => {
  930. const link = document.createElement('a');
  931. link.target = '_blank';
  932. link.href = url;
  933. link.rel = 'noreferrer noopener';
  934. const evt = document.createEvent('MouseEvents');
  935. evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  936. appendClickRemove(link, evt);
  937. };
  938. const getLink = (editor, elm) => editor.dom.getParent(elm, 'a[href]');
  939. const getSelectedLink = editor => getLink(editor, editor.selection.getStart());
  940. const hasOnlyAltModifier = e => {
  941. return e.altKey === true && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false;
  942. };
  943. const gotoLink = (editor, a) => {
  944. if (a) {
  945. const href = getHref(a);
  946. if (/^#/.test(href)) {
  947. const targetEl = editor.dom.select(href);
  948. if (targetEl.length) {
  949. editor.selection.scrollIntoView(targetEl[0], true);
  950. }
  951. } else {
  952. open(a.href);
  953. }
  954. }
  955. };
  956. const openDialog = editor => () => {
  957. open$1(editor);
  958. };
  959. const gotoSelectedLink = editor => () => {
  960. gotoLink(editor, getSelectedLink(editor));
  961. };
  962. const setupGotoLinks = editor => {
  963. editor.on('click', e => {
  964. const link = getLink(editor, e.target);
  965. if (link && global$4.metaKeyPressed(e)) {
  966. e.preventDefault();
  967. gotoLink(editor, link);
  968. }
  969. });
  970. editor.on('keydown', e => {
  971. const link = getSelectedLink(editor);
  972. if (link && e.keyCode === 13 && hasOnlyAltModifier(e)) {
  973. e.preventDefault();
  974. gotoLink(editor, link);
  975. }
  976. });
  977. };
  978. const toggleState = (editor, toggler) => {
  979. editor.on('NodeChange', toggler);
  980. return () => editor.off('NodeChange', toggler);
  981. };
  982. const toggleActiveState = editor => api => {
  983. const updateState = () => api.setActive(!editor.mode.isReadOnly() && getAnchorElement(editor, editor.selection.getNode()) !== null);
  984. updateState();
  985. return toggleState(editor, updateState);
  986. };
  987. const toggleEnabledState = editor => api => {
  988. const updateState = () => api.setEnabled(getAnchorElement(editor, editor.selection.getNode()) !== null);
  989. updateState();
  990. return toggleState(editor, updateState);
  991. };
  992. const toggleUnlinkState = editor => api => {
  993. const hasLinks$1 = parents => hasLinks(parents) || hasLinksInSelection(editor.selection.getRng());
  994. const parents = editor.dom.getParents(editor.selection.getStart());
  995. api.setEnabled(hasLinks$1(parents));
  996. return toggleState(editor, e => api.setEnabled(hasLinks$1(e.parents)));
  997. };
  998. const register = editor => {
  999. editor.addCommand('mceLink', () => {
  1000. if (useQuickLink(editor)) {
  1001. editor.dispatch('contexttoolbar-show', { toolbarKey: 'quicklink' });
  1002. } else {
  1003. openDialog(editor)();
  1004. }
  1005. });
  1006. };
  1007. const setup = editor => {
  1008. editor.addShortcut('Meta+K', '', () => {
  1009. editor.execCommand('mceLink');
  1010. });
  1011. };
  1012. const setupButtons = editor => {
  1013. editor.ui.registry.addToggleButton('link', {
  1014. icon: 'link',
  1015. tooltip: 'Insert/edit link',
  1016. onAction: openDialog(editor),
  1017. onSetup: toggleActiveState(editor)
  1018. });
  1019. editor.ui.registry.addButton('openlink', {
  1020. icon: 'new-tab',
  1021. tooltip: 'Open link',
  1022. onAction: gotoSelectedLink(editor),
  1023. onSetup: toggleEnabledState(editor)
  1024. });
  1025. editor.ui.registry.addButton('unlink', {
  1026. icon: 'unlink',
  1027. tooltip: 'Remove link',
  1028. onAction: () => unlink(editor),
  1029. onSetup: toggleUnlinkState(editor)
  1030. });
  1031. };
  1032. const setupMenuItems = editor => {
  1033. editor.ui.registry.addMenuItem('openlink', {
  1034. text: 'Open link',
  1035. icon: 'new-tab',
  1036. onAction: gotoSelectedLink(editor),
  1037. onSetup: toggleEnabledState(editor)
  1038. });
  1039. editor.ui.registry.addMenuItem('link', {
  1040. icon: 'link',
  1041. text: 'Link...',
  1042. shortcut: 'Meta+K',
  1043. onAction: openDialog(editor)
  1044. });
  1045. editor.ui.registry.addMenuItem('unlink', {
  1046. icon: 'unlink',
  1047. text: 'Remove link',
  1048. onAction: () => unlink(editor),
  1049. onSetup: toggleUnlinkState(editor)
  1050. });
  1051. };
  1052. const setupContextMenu = editor => {
  1053. const inLink = 'link unlink openlink';
  1054. const noLink = 'link';
  1055. editor.ui.registry.addContextMenu('link', { update: element => hasLinks(editor.dom.getParents(element, 'a')) ? inLink : noLink });
  1056. };
  1057. const setupContextToolbars = editor => {
  1058. const collapseSelectionToEnd = editor => {
  1059. editor.selection.collapse(false);
  1060. };
  1061. const onSetupLink = buttonApi => {
  1062. const node = editor.selection.getNode();
  1063. buttonApi.setEnabled(getAnchorElement(editor, node) !== null);
  1064. return noop;
  1065. };
  1066. const getLinkText = value => {
  1067. const anchor = getAnchorElement(editor);
  1068. const onlyText = isOnlyTextSelected(editor);
  1069. if (!anchor && onlyText) {
  1070. const text = getAnchorText(editor.selection, anchor);
  1071. return Optional.some(text.length > 0 ? text : value);
  1072. } else {
  1073. return Optional.none();
  1074. }
  1075. };
  1076. editor.ui.registry.addContextForm('quicklink', {
  1077. launch: {
  1078. type: 'contextformtogglebutton',
  1079. icon: 'link',
  1080. tooltip: 'Link',
  1081. onSetup: toggleActiveState(editor)
  1082. },
  1083. label: 'Link',
  1084. predicate: node => !!getAnchorElement(editor, node) && hasContextToolbar(editor),
  1085. initValue: () => {
  1086. const elm = getAnchorElement(editor);
  1087. return !!elm ? getHref(elm) : '';
  1088. },
  1089. commands: [
  1090. {
  1091. type: 'contextformtogglebutton',
  1092. icon: 'link',
  1093. tooltip: 'Link',
  1094. primary: true,
  1095. onSetup: buttonApi => {
  1096. const node = editor.selection.getNode();
  1097. buttonApi.setActive(!!getAnchorElement(editor, node));
  1098. return toggleActiveState(editor)(buttonApi);
  1099. },
  1100. onAction: formApi => {
  1101. const value = formApi.getValue();
  1102. const text = getLinkText(value);
  1103. const attachState = {
  1104. href: value,
  1105. attach: noop
  1106. };
  1107. link(editor, attachState, {
  1108. href: value,
  1109. text,
  1110. title: Optional.none(),
  1111. rel: Optional.none(),
  1112. target: Optional.none(),
  1113. class: Optional.none()
  1114. });
  1115. collapseSelectionToEnd(editor);
  1116. formApi.hide();
  1117. }
  1118. },
  1119. {
  1120. type: 'contextformbutton',
  1121. icon: 'unlink',
  1122. tooltip: 'Remove link',
  1123. onSetup: onSetupLink,
  1124. onAction: formApi => {
  1125. unlink(editor);
  1126. formApi.hide();
  1127. }
  1128. },
  1129. {
  1130. type: 'contextformbutton',
  1131. icon: 'new-tab',
  1132. tooltip: 'Open link',
  1133. onSetup: onSetupLink,
  1134. onAction: formApi => {
  1135. gotoSelectedLink(editor)();
  1136. formApi.hide();
  1137. }
  1138. }
  1139. ]
  1140. });
  1141. };
  1142. var Plugin = () => {
  1143. global$5.add('link', editor => {
  1144. register$1(editor);
  1145. setupButtons(editor);
  1146. setupMenuItems(editor);
  1147. setupContextMenu(editor);
  1148. setupContextToolbars(editor);
  1149. setupGotoLinks(editor);
  1150. register(editor);
  1151. setup(editor);
  1152. });
  1153. };
  1154. Plugin();
  1155. })();