plugin.js 38 KB

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