/controls/richtexteditor/src/markdown-parser/plugin/md-selection-formats.ts

https://github.com/syncfusion/ej2-javascript-ui-controls · TypeScript · 336 lines · 323 code · 2 blank · 11 comment · 186 complexity · c231e9297aad1a40c8dc66c87f18999d MD5 · raw file

  1. import { isNullOrUndefined, KeyboardEventArgs } from '@syncfusion/ej2-base';
  2. import { MarkdownParser } from './../base/markdown-parser';
  3. import * as CONSTANT from './../base/constant';
  4. import { IMarkdownSubCommands, IMDKeyboardEvent, IMDFormats } from './../base/interface';
  5. import { MarkdownSelection } from './markdown-selection';
  6. import { extend } from '@syncfusion/ej2-base';
  7. import * as EVENTS from './../../common/constant';
  8. /**
  9. * SelectionCommands internal component
  10. *
  11. * @hidden
  12. * @deprecated
  13. */
  14. export class MDSelectionFormats {
  15. private parent: MarkdownParser;
  16. private selection: MarkdownSelection;
  17. public syntax: { [key: string]: string };
  18. private currentAction: string;
  19. public constructor(parent: IMDFormats) {
  20. extend(this, this, parent, true);
  21. this.selection = this.parent.markdownSelection;
  22. this.addEventListener();
  23. }
  24. private addEventListener(): void {
  25. this.parent.observer.on(CONSTANT.selectionCommand, this.applyCommands, this);
  26. this.parent.observer.on(EVENTS.KEY_DOWN_HANDLER, this.keyDownHandler, this);
  27. }
  28. private keyDownHandler(e: IMDKeyboardEvent): void {
  29. switch ((e.event as KeyboardEventArgs).action) {
  30. case 'bold':
  31. this.applyCommands({ subCommand: 'Bold', callBack: e.callBack });
  32. e.event.preventDefault();
  33. break;
  34. case 'italic':
  35. this.applyCommands({ subCommand: 'Italic', callBack: e.callBack });
  36. e.event.preventDefault();
  37. break;
  38. case 'strikethrough':
  39. this.applyCommands({ subCommand: 'StrikeThrough', callBack: e.callBack });
  40. e.event.preventDefault();
  41. break;
  42. case 'uppercase':
  43. this.applyCommands({ subCommand: 'UpperCase', callBack: e.callBack });
  44. e.event.preventDefault();
  45. break;
  46. case 'lowercase':
  47. this.applyCommands({ subCommand: 'LowerCase', callBack: e.callBack });
  48. e.event.preventDefault();
  49. break;
  50. case 'superscript':
  51. this.applyCommands({ subCommand: 'SuperScript', callBack: e.callBack });
  52. e.event.preventDefault();
  53. break;
  54. case 'subscript':
  55. this.applyCommands({ subCommand: 'SubScript', callBack: e.callBack });
  56. e.event.preventDefault();
  57. break;
  58. }
  59. }
  60. private isBold(text: string, cmd: string): boolean {
  61. return text.search('\\' + cmd + '\\' + cmd + '') !== -1;
  62. }
  63. private isItalic(text: string, cmd: string): boolean {
  64. return text.search('\\' + cmd) !== -1;
  65. }
  66. private isMatch(text: string, cmd: string): string[] {
  67. let matchText: string[] = [''];
  68. switch (cmd) {
  69. case this.syntax.Italic:
  70. matchText = text.match(this.singleCharRegx(cmd));
  71. break;
  72. case this.syntax.InlineCode:
  73. matchText = text.match(this.singleCharRegx(cmd));
  74. break;
  75. case this.syntax.StrikeThrough:
  76. matchText = text.match(this.singleCharRegx(cmd));
  77. break;
  78. }
  79. return matchText;
  80. }
  81. private multiCharRegx(cmd: string): RegExp {
  82. return new RegExp('(\\' + cmd + '\\' + cmd + ')', 'g');
  83. }
  84. private singleCharRegx(cmd: string): RegExp {
  85. return new RegExp('(\\' + cmd + ')', 'g');
  86. }
  87. public isAppliedCommand(cmd?: string): | boolean {
  88. // eslint-disable-next-line
  89. const selectCmd: string = '';
  90. let isFormat: boolean = false;
  91. const textArea: HTMLTextAreaElement = this.parent.element as HTMLTextAreaElement;
  92. const start: number = textArea.selectionStart;
  93. const splitAt: Function = (index: number) => (x: string) => [x.slice(0, index), x.slice(index)];
  94. const splitText: string[] = splitAt(start)(textArea.value);
  95. const cmdB: string = this.syntax.Bold.substr(0, 1);
  96. const cmdI: string = this.syntax.Italic;
  97. const selectedText: string = this.parent.markdownSelection.getSelectedText(textArea);
  98. if (selectedText !== '' && selectedText === selectedText.toLocaleUpperCase() && cmd === 'UpperCase') {
  99. return true;
  100. } else if (selectedText === '') {
  101. const beforeText: string = textArea.value.substr(splitText[0].length - 1, 1);
  102. const afterText: string = splitText[1].substr(0, 1);
  103. if ((beforeText !== '' && afterText !== '' && beforeText.match(/[a-z]/i)) &&
  104. beforeText === beforeText.toLocaleUpperCase() && afterText === afterText.toLocaleUpperCase() && cmd === 'UpperCase') {
  105. return true;
  106. }
  107. }
  108. if (!(this.isBold(splitText[0], cmdB)) && !(this.isItalic(splitText[0], cmdI)) && !(this.isBold(splitText[1], cmdB)) &&
  109. !(this.isItalic(splitText[1], cmdI))) {
  110. if ((!isNullOrUndefined(this.isMatch(splitText[0], this.syntax.StrikeThrough)) &&
  111. !isNullOrUndefined(this.isMatch(splitText[1], this.syntax.StrikeThrough))) &&
  112. (this.isMatch(splitText[0], this.syntax.StrikeThrough).length % 2 === 1 &&
  113. this.isMatch(splitText[1], this.syntax.StrikeThrough).length % 2 === 1) && cmd === 'StrikeThrough') {
  114. isFormat = true;
  115. }
  116. if ((!isNullOrUndefined(this.isMatch(splitText[0], this.syntax.InlineCode)) &&
  117. !isNullOrUndefined(this.isMatch(splitText[1], this.syntax.InlineCode))) &&
  118. (this.isMatch(splitText[0], this.syntax.InlineCode).length % 2 === 1 &&
  119. this.isMatch(splitText[1], this.syntax.InlineCode).length % 2 === 1) && cmd === 'InlineCode') {
  120. isFormat = true;
  121. }
  122. /* eslint-disable */
  123. if ((!isNullOrUndefined(splitText[0].match(/\<sub>/g)) && !isNullOrUndefined(splitText[1].match(/\<\/sub>/g))) &&
  124. (splitText[0].match(/\<sub>/g).length % 2 === 1 &&
  125. splitText[1].match(/\<\/sub>/g).length % 2 === 1) && cmd === 'SubScript') {
  126. isFormat = true;
  127. }
  128. if ((!isNullOrUndefined(splitText[0].match(/\<sup>/g)) && !isNullOrUndefined(splitText[1].match(/\<\/sup>/g))) &&
  129. (splitText[0].match(/\<sup>/g).length % 2 === 1 && splitText[1].match(/\<\/sup>/g).length % 2 === 1) &&
  130. cmd === 'SuperScript') {
  131. isFormat = true;
  132. }
  133. /* eslint-enable */
  134. }
  135. if ((this.isBold(splitText[0], cmdB) && this.isBold(splitText[1], cmdB)) &&
  136. (splitText[0].match(this.multiCharRegx(cmdB)).length % 2 === 1 &&
  137. splitText[1].match(this.multiCharRegx(cmdB)).length % 2 === 1) && cmd === 'Bold') {
  138. isFormat = true;
  139. }
  140. splitText[0] = this.isBold(splitText[0], cmdB) ? splitText[0].replace(this.multiCharRegx(cmdB), '$%@') : splitText[0];
  141. splitText[1] = this.isBold(splitText[1], cmdB) ? splitText[1].replace(this.multiCharRegx(cmdB), '$%@') : splitText[1];
  142. if ((!isNullOrUndefined(this.isMatch(splitText[0], this.syntax.Italic)) &&
  143. !isNullOrUndefined(this.isMatch(splitText[1], this.syntax.Italic))) &&
  144. (this.isMatch(splitText[0], this.syntax.Italic).length % 2 === 1 &&
  145. this.isMatch(splitText[1], this.syntax.Italic).length % 2 === 1) && cmd === 'Italic') {
  146. isFormat = true;
  147. }
  148. if ((!isNullOrUndefined(this.isMatch(splitText[0], this.syntax.StrikeThrough)) &&
  149. !isNullOrUndefined(this.isMatch(splitText[1], this.syntax.StrikeThrough))) &&
  150. (this.isMatch(splitText[0], this.syntax.StrikeThrough).length % 2 === 1 &&
  151. this.isMatch(splitText[1], this.syntax.StrikeThrough).length % 2 === 1) && cmd === 'StrikeThrough') {
  152. isFormat = true;
  153. }
  154. if ((!isNullOrUndefined(this.isMatch(splitText[0], this.syntax.InlineCode)) &&
  155. !isNullOrUndefined(this.isMatch(splitText[1], this.syntax.InlineCode))) &&
  156. (this.isMatch(splitText[0], this.syntax.InlineCode).length % 2 === 1 &&
  157. this.isMatch(splitText[1], this.syntax.InlineCode).length % 2 === 1) && cmd === 'InlineCode') {
  158. isFormat = true;
  159. }
  160. /* eslint-disable */
  161. if ((!isNullOrUndefined(splitText[0].match(/\<sub>/g)) && !isNullOrUndefined(splitText[1].match(/\<\/sub>/g))) &&
  162. (splitText[0].match(/\<sub>/g).length % 2 === 1 && splitText[1].match(/\<\/sub>/g).length % 2 === 1) && cmd === 'SubScript') {
  163. isFormat = true;
  164. }
  165. if ((!isNullOrUndefined(splitText[0].match(/\<sup>/g)) && !isNullOrUndefined(splitText[1].match(/\<\/sup>/g))) &&
  166. (splitText[0].match(/\<sup>/g).length % 2 === 1 && splitText[1].match(/\<\/sup>/g).length % 2 === 1) && cmd === 'SuperScript') {
  167. isFormat = true;
  168. /* eslint-enable */
  169. }
  170. return isFormat;
  171. }
  172. private applyCommands(e: IMarkdownSubCommands): void {
  173. this.currentAction = e.subCommand;
  174. const textArea: HTMLTextAreaElement = this.parent.element as HTMLTextAreaElement;
  175. this.selection.save(textArea.selectionStart, textArea.selectionEnd);
  176. const start: number = textArea.selectionStart;
  177. const end: number = textArea.selectionEnd;
  178. let addedLength: number = 0;
  179. const selection: { [key: string]: string | number } = this.parent.markdownSelection.getSelectedInlinePoints(textArea);
  180. if (this.isAppliedCommand(e.subCommand) && selection.text !== '') {
  181. const startCmd: string = this.syntax[e.subCommand];
  182. const endCmd: string = e.subCommand === 'SubScript' ? '</sub>' :
  183. e.subCommand === 'SuperScript' ? '</sup>' : this.syntax[e.subCommand];
  184. const startLength: number = (e.subCommand === 'UpperCase' || e.subCommand === 'LowerCase') ? 0 : startCmd.length;
  185. const startNo: number = textArea.value.substr(0, selection.start as number).lastIndexOf(startCmd);
  186. let endNo: number = textArea.value.substr(selection.end as number, textArea.value.length).indexOf(endCmd);
  187. endNo = endNo + (selection.end as number);
  188. const repStartText: string = this.replaceAt(
  189. textArea.value.substr(0, selection.start as number), startCmd, '', startNo, selection.start as number);
  190. const repEndText: string = this.replaceAt(
  191. textArea.value.substr(selection.end as number, textArea.value.length), endCmd, '', 0, endNo);
  192. textArea.value = repStartText + selection.text + repEndText;
  193. this.restore(textArea, start - startLength, end - startLength, e);
  194. return;
  195. }
  196. if (selection.text !== '' && !this.isApplied(selection, e.subCommand)) {
  197. addedLength = (e.subCommand === 'UpperCase' || e.subCommand === 'LowerCase') ? 0 :
  198. this.syntax[e.subCommand].length;
  199. const repStart: string = textArea.value.substr(
  200. selection.start as number - this.syntax[e.subCommand].length, this.syntax[e.subCommand].length);
  201. let repEnd: string;
  202. if ((repStart === e.subCommand) || ((selection.start as number - this.syntax[e.subCommand].length ===
  203. textArea.value.indexOf(this.syntax[e.subCommand])) && (selection.end as number === textArea.value.lastIndexOf(
  204. this.syntax[e.subCommand]) || selection.end as number === textArea.value.lastIndexOf(
  205. '</' + this.syntax[e.subCommand].substring(1, 5))))) {
  206. if (e.subCommand === 'SubScript' || e.subCommand === 'SuperScript') {
  207. repEnd = textArea.value.substr(selection.end as number, this.syntax[e.subCommand].length + 1);
  208. } else {
  209. repEnd = textArea.value.substr(selection.end as number, this.syntax[e.subCommand].length);
  210. }
  211. const repStartText: string = this.replaceAt(
  212. textArea.value.substr(0, selection.start as number),
  213. repStart, '', selection.start as number - this.syntax[e.subCommand].length, selection.start as number);
  214. const repEndText: string = this.replaceAt(
  215. textArea.value.substr(selection.end as number, textArea.value.length), repEnd, '', 0, repEnd.length);
  216. textArea.value = repStartText + selection.text + repEndText;
  217. this.restore(textArea, start - addedLength, end - addedLength, e);
  218. } else {
  219. if (e.subCommand === 'SubScript' || e.subCommand === 'SuperScript') {
  220. selection.text = this.syntax[e.subCommand] + selection.text
  221. + '</' + this.syntax[e.subCommand].substring(1, 5);
  222. } else if (e.subCommand === 'UpperCase' || e.subCommand === 'LowerCase') {
  223. selection.text = (e.subCommand === 'UpperCase') ? (selection.text as string).toUpperCase()
  224. : (selection.text as string).toLowerCase();
  225. } else {
  226. selection.text = this.syntax[e.subCommand] + selection.text + this.syntax[e.subCommand];
  227. }
  228. textArea.value = textArea.value.substr(0, selection.start as number) + selection.text +
  229. textArea.value.substr(selection.end as number, textArea.value.length);
  230. this.restore(textArea, start + addedLength, end + addedLength, e);
  231. }
  232. } else if (e.subCommand !== 'UpperCase' && e.subCommand !== 'LowerCase') {
  233. if (e.subCommand === 'SubScript' || e.subCommand === 'SuperScript') {
  234. selection.text = this.textReplace(selection.text as string, e.subCommand);
  235. selection.text = this.syntax[e.subCommand] + selection.text
  236. + '</' + this.syntax[e.subCommand].substring(1, 5);
  237. } else {
  238. selection.text = this.textReplace(selection.text as string, e.subCommand);
  239. selection.text = this.syntax[e.subCommand] + selection.text + this.syntax[e.subCommand];
  240. }
  241. textArea.value = textArea.value.substr(0, selection.start as number)
  242. + selection.text + textArea.value.substr(selection.end as number, textArea.value.length);
  243. addedLength = this.syntax[e.subCommand].length;
  244. if (selection.start === selection.end) {
  245. this.restore(textArea, start + addedLength, end + addedLength, e);
  246. } else {
  247. this.restore(textArea, start + addedLength, end - addedLength, e);
  248. }
  249. } else {
  250. this.restore(textArea, start, end, e);
  251. }
  252. this.parent.undoRedoManager.saveData();
  253. }
  254. private replaceAt(input: string, search: string, replace: string, start: number, end: number): string {
  255. return input.slice(0, start)
  256. + input.slice(start, end).replace(search, replace)
  257. + input.slice(end);
  258. }
  259. private restore(textArea: HTMLTextAreaElement, start: number, end: number, event?: IMarkdownSubCommands | IMDKeyboardEvent): void {
  260. this.selection.save(start, end);
  261. this.selection.restore(textArea);
  262. if (event && event.callBack) {
  263. event.callBack({
  264. requestType: this.currentAction,
  265. selectedText: this.selection.getSelectedText(textArea),
  266. editorMode: 'Markdown',
  267. event: event.event
  268. });
  269. }
  270. }
  271. private textReplace(text: string, command: string): string {
  272. let regx: RegExp = this.singleCharRegx(this.syntax[command]);
  273. switch (command) {
  274. case 'Bold':
  275. regx = this.multiCharRegx(this.syntax[command].substr(0, 1));
  276. text = text.replace(regx, '');
  277. break;
  278. case 'Italic':
  279. if (!this.isBold(text, this.syntax[command].substr(0, 1))) {
  280. text = text.replace(regx, '');
  281. } else {
  282. const regxB: RegExp = this.multiCharRegx(this.syntax[command].substr(0, 1));
  283. let repText: string = text;
  284. repText = repText.replace(regxB, '$%@').replace(regx, '');
  285. const regxTemp: RegExp = new RegExp('\\$%@', 'g');
  286. text = repText.replace(regxTemp, this.syntax[command].substr(0, 1) + this.syntax[command].substr(0, 1));
  287. }
  288. break;
  289. case 'StrikeThrough':
  290. text = text.replace(regx, '');
  291. break;
  292. case 'InlineCode':
  293. text = text.replace(regx, '');
  294. break;
  295. case 'SubScript':
  296. text = text.replace(/<sub>/g, '').replace(/<\/sub>/g, '');
  297. break;
  298. case 'SuperScript':
  299. text = text.replace(/<sup>/g, '').replace(/<\/sup>/g, '');
  300. break;
  301. }
  302. return text;
  303. }
  304. private isApplied(line: { [key: string]: string | number }, command: string): boolean | void {
  305. let regx: RegExp = this.singleCharRegx(this.syntax[command]);
  306. switch (command) {
  307. case 'SubScript':
  308. case 'SuperScript':
  309. regx = this.singleCharRegx(this.syntax[command]);
  310. return regx.test(line.text as string);
  311. case 'Bold':
  312. case 'StrikeThrough':
  313. regx = this.multiCharRegx(this.syntax[command].substr(0, 1));
  314. return regx.test(line.text as string);
  315. case 'UpperCase':
  316. case 'LowerCase':
  317. regx = new RegExp('^[' + this.syntax[command] + ']*$', 'g');
  318. return regx.test(line.text as string);
  319. case 'Italic': {
  320. let regTest: boolean;
  321. const regxB: RegExp = this.multiCharRegx(this.syntax[command].substr(0, 1));
  322. if (regxB.test(line.text as string)) {
  323. let repText: string = line.text as string;
  324. repText = repText.replace(regxB, '$%#');
  325. regTest = regx.test(repText);
  326. } else {
  327. regTest = regx.test(line.text as string);
  328. }
  329. return regTest; }
  330. case 'InlineCode':
  331. return regx.test(line.text as string);
  332. }
  333. }
  334. }