modx.texteditor.js 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123
  1. Ext.ux.Ace = Ext.extend(Ext.form.TextField, {
  2. growMin : 60,
  3. growMax: 1000,
  4. mode : 'text',
  5. theme : 'textmate',
  6. showInvisibles : false,
  7. selectionStyle : 'line',
  8. scrollSpeed : 3,
  9. showFoldWidgets : true,
  10. useSoftTabs : true,
  11. tabSize : 4,
  12. useWrapMode : false,
  13. fontSize : '13px',
  14. value : '',
  15. style: 'padding:0',
  16. initEvents : function(){
  17. Ext.ux.Ace.superclass.initEvents.call(this);
  18. this.editor.on('focus', this.onFocus.bind(this));
  19. this.editor.on('blur', this.onBlur.bind(this));
  20. },
  21. initComponent : function(){
  22. this.valueHolder = document.createElement('input');
  23. this.valueHolder.type = 'hidden';
  24. this.valueHolder.name = this.name;
  25. this.valueHolder.value = this.value;
  26. },
  27. onRender : function(ct, position){
  28. if(!this.el){
  29. this.defaultAutoCreate = {
  30. tag: "div",
  31. cls: "x-form-textarea",
  32. style:"width:100%;height:60px"
  33. };
  34. }
  35. Ext.ux.Ace.superclass.onRender.call(this, ct, position);
  36. var useragent = ace.require('ace/lib/useragent');
  37. if(this.grow){
  38. this.el.setHeight(this.growMin);
  39. }
  40. this.editor = ace.edit(this.el.dom);
  41. this.editor.$blockScrolling = Infinity;
  42. this.el.appendChild(this.valueHolder);
  43. this.el.dom.removeAttribute('name');
  44. this.el.focus = this.focus.bind(this);
  45. this.editor.getSession().setValue(this.valueHolder.value);
  46. this.editor.setShowPrintMargin(false);
  47. this.editor.getSession().setTabSize(this.tabSize);
  48. this.editor.setAutoScrollEditorIntoView(true);
  49. if (!useragent.isMac)
  50. this.editor.setDragDelay(0);
  51. this.editor.setFontSize(this.fontSize);
  52. this.editor.setFadeFoldWidgets(false);
  53. this.setShowInvisibles(this.showInvisibles);
  54. this.setSelectionStyle(this.selectionStyle);
  55. this.setScrollSpeed(this.scrollSpeed);
  56. this.setShowFoldWidgets(this.showFoldWidgets);
  57. this.setUseSoftTabs(this.useSoftTabs);
  58. this.setUseWrapMode(this.useWrapMode);
  59. ace.require("ace/ext/language_tools");
  60. this.editor.setOptions({
  61. enableBasicAutocompletion: true
  62. });
  63. this.setTheme(this.theme);
  64. this.setMode(this.mode);
  65. this.editor.getSession().on('change', (function(){
  66. setTimeout(function(){
  67. this.valueHolder.value = this.editor.getSession().getValue();
  68. }.bind(this), 10);
  69. }).bind(this));
  70. // TODO: attach autoSize to according event (?)
  71. this.autoSize();
  72. },
  73. onDestroy : function(){
  74. this.editor.destroy();
  75. Ext.ux.Ace.superclass.onDestroy.call(this);
  76. },
  77. validate : function(){
  78. return true;
  79. },
  80. getErrors : function(value){
  81. return null;
  82. },
  83. onResize : function(){
  84. this.editor.resize(true);
  85. },
  86. doAutoSize : function(e){
  87. return !e.isNavKeyPress() || e.getKey() == e.ENTER;
  88. },
  89. autoSize: function(){
  90. var linesCount = this.editor.getSession().getScreenLength();
  91. var lineHeight = this.editor.renderer.lineHeight;
  92. var scrollBar = this.editor.renderer.scrollBar.getWidth();
  93. var bordersWidth = this.el.getBorderWidth('tb');
  94. var bottomOffset = lineHeight*5+scrollBar;
  95. var h = Math.min(this.growMax, Math.max(linesCount * lineHeight + bordersWidth + bottomOffset, this.growMin));
  96. var heightChanged = h!=this.lastHeight;
  97. if(this.grow && heightChanged){
  98. this.setHeight(h);
  99. this.editor.resize();
  100. this.fireEvent("autosize", this, h);
  101. }
  102. if(!this.editor.searchBox || heightChanged){
  103. if(this.editor.searchBox)this.detectSearchBoxPosition(h);
  104. else{
  105. var that = this;
  106. ace.config.loadModule("ace/ext/searchbox",function(m){
  107. m.Search(that.editor);
  108. that.editor.searchBox.hide();
  109. that.detectSearchBoxPosition(h);
  110. });
  111. }
  112. }
  113. if(heightChanged)this.lastHeight = h;
  114. },
  115. detectSearchBoxPosition : function(editorHeight){
  116. var triggerOffset = 150;
  117. var defaultStyles={
  118. position:null
  119. ,bottom:null
  120. ,top:null
  121. ,borderRadius:null
  122. };
  123. var fixedStyles={
  124. position:'fixed'
  125. ,bottom:'0'
  126. ,top:'initial'
  127. ,borderRadius:'5px 0px 0px 0'
  128. };
  129. if(!this.isFullscreen&&editorHeight>=(window.innerHeight-triggerOffset))Ext.apply(this.editor.searchBox.element.style,fixedStyles);
  130. else Ext.apply(this.editor.searchBox.element.style,defaultStyles);
  131. },
  132. setSize : function(width, height){
  133. Ext.ux.Ace.superclass.setSize.apply(this, arguments);
  134. this.editor.resize(true);
  135. },
  136. getValue : function (){
  137. return this.valueHolder.value;
  138. },
  139. setValue : function (value){
  140. if (this.editor) {
  141. this.editor.getSession().setValue(value);
  142. } else {
  143. this.valueHolder.value = value;
  144. }
  145. this.value = value;
  146. },
  147. setMode : function (mode){
  148. this.editor.getSession().setMode( 'ace/mode/' + mode );
  149. },
  150. setTheme : function(theme){
  151. this.editor.setTheme('ace/theme/' + theme);
  152. },
  153. setFontSize : function(fontSize){
  154. this.editor.setFontSize(fontSize);
  155. },
  156. setShowInvisibles : function(showInvisibles){
  157. this.editor.setShowInvisibles(showInvisibles);
  158. },
  159. setSelectionStyle : function(selectionStyle){
  160. this.editor.setSelectionStyle(selectionStyle);
  161. },
  162. setScrollSpeed : function(scrollSpeed){
  163. this.editor.setScrollSpeed(scrollSpeed);
  164. },
  165. setShowFoldWidgets : function(showFoldWidgets){
  166. this.editor.setShowFoldWidgets(showFoldWidgets);
  167. },
  168. setUseSoftTabs : function(useSoftTabs){
  169. this.editor.getSession().setUseSoftTabs(useSoftTabs);
  170. },
  171. setUseWrapMode : function(useWrapMode){
  172. this.editor.getSession().setUseWrapMode(useWrapMode);
  173. },
  174. insertAtCursor : function (value){
  175. return this.editor.insert(value);
  176. },
  177. focus: function (){
  178. this.editor.focus();
  179. },
  180. blur: function (){
  181. this.editor.blur();
  182. }
  183. });
  184. Ext.reg('ace', Ext.ux.Ace);
  185. Ext.namespace('MODx.ux');
  186. MODx.ux.Ace = Ext.extend(Ext.ux.Ace, {
  187. mimeType : 'text/plain',
  188. theme : MODx.config['ace.theme'] || 'textmate',
  189. fontSize : MODx.config['ace.font_size'] || '13px',
  190. useWrapMode : MODx.config['ace.word_wrap'] == true,
  191. useSoftTabs : MODx.config['ace.soft_tabs'] == true,
  192. tabSize : MODx.config['ace.tab_size'] * 1 || 4,
  193. showFoldWidgets : MODx.config['ace.fold_widgets'] == true,
  194. showInvisibles : MODx.config['ace.show_invisibles'] == true,
  195. modxTags : false,
  196. initComponent : function() {
  197. MODx.ux.Ace.superclass.initComponent.call(this);
  198. var config = ace.require("ace/config");
  199. var acePath = MODx.config['assets_url'] + 'components/ace/ace';
  200. config.set('basePath', acePath);
  201. config.set('modePath', acePath);
  202. config.set('themePath', acePath);
  203. config.set('workerPath', acePath);
  204. if(MODx.config['ace.grow']!==undefined&&MODx.config['ace.grow']!==''){
  205. this.grow = true;
  206. this.growMax = parseInt(MODx.config['ace.grow'])||Infinity;
  207. this.growMin = this.height;
  208. }
  209. this.windows = [];
  210. },
  211. onRender : function (ct, position) {
  212. MODx.ux.Ace.superclass.onRender.call(this, ct, position);
  213. var TokenIterator = ace.require("ace/token_iterator").TokenIterator;
  214. var userAgent = ace.require("ace/lib/useragent");
  215. var shortcut = (userAgent.isMac ? 'Command + F12' : 'Ctrl + F11');
  216. this.maximizeTitle = _('ui_ace.maximize') + ' (' + shortcut + ')';
  217. this.minimizeTitle = _('ui_ace.minimize') + ' (' + shortcut + ')';
  218. this.maximizer = document.createElement('div');
  219. this.maximizer.title = this.maximizeTitle;
  220. this.maximizer.className = 'ace_maximizer';
  221. this.maximizer.onmousedown = function(event) {
  222. event.preventDefault();
  223. };
  224. this.maximizer.onclick = function(event) {
  225. this.fullScreen();
  226. event.preventDefault();
  227. }.bind(this);
  228. this.editor.renderer.scroller.appendChild(this.maximizer);
  229. this.setMimeType(this.mimeType);
  230. if (!MODx.ux.Ace.initialized) {
  231. var style = "\
  232. .ace_maximized {position: fixed; border: none; top: 0; left: 0; right: 0; bottom: 0; width: auto !important; height: auto !important;z-index: 100}\
  233. .ace_maximizer {position: absolute; width: 16px; height: 16px; top: 3px; right: 3px; opacity: 0.7; z-index: 10; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABgFBMVEVmrBxkqxpjqhlkqxprryFnrB1kqxtkqxp6uzJiqRhZow9kqxpmrBxlrBtnrBxjqhlmrBxjqhptsSNkqhplqxt4tyxkqhlmrB1mrBx5uzJmrBxorR9orR5nrB1kqhlorR14uS5mrRxhqBhmrR1aoxBlqxt3titusiRlqxpiqRd4ty1rrh9hqBdjqxp3tSpaow9mrR15ujBorh5lrBxapRFmrBxkqhpjqhp5uzBhqRdnrB1rsCJlrBt6vDNorh93tipmqxxhqRhapBFbpRJnrR1mrB14uC1usiVnrB1apRFhqReq1VmLxUGv5Gyt3GOs22Cm0FKn2FyUzUyRykev4WmUz1CUy0mw5G6RzEmr11xusSOSy0iOxkKPx0Or2V6CwTuo3GGUz0+Sy0mu5W6Ry0mq1lptsCORy0iNx0OAvDOo2V5/vDOCwDmPxkOOy0mj0lWl1lqu4Gap1Viv4mms2l+s3WSPx0SKwDtusiWCwTqQxUGSyUaRykiAvDSp2l+Qx0QdWpRIAAAAS3RSTlMAAAAa4gAAlfAZMRLhuQCV1ZXwGgDwlQCy8MzY2swS2PDqEdUisvDwABn64hEA8CLh+tq5MeoAAPAZ4eKy+tr6uREiMdXq8PDqIhHgP7bQAAAA4ElEQVR42mOw5uBw52cAA351CwUWBttk30p9iIBKTmGcFgNHeEKwlBIfAwOrjmxNQaQoQ0V8TVa9PDMDg7B0RF1MdhSDGJdLfWqVqS6Ta5BfvYAzO1Arp0xVOS8bo3FGtAwnxDBuHhsRBgYNVR5uBihgZAOTTDA+AxMjiDSDCYhzW0kAtTBKGMiJgwUs7cJ8eBkZHfISjTSBXDEu5RT/akVzJo/Q4mgBE3aGisr0OqjDkupyM9MYJEMCY6UcQU73ki3LL1JjMCwtqfWEmO5U6x1gz8Ci4CYkCBEQFBLV0wYAXu4m8P20SwoAAAAASUVORK5CYII=)}\
  234. .ace_maximizer:hover {opacity: 1}\
  235. ";
  236. new MODx.ux.Ace.CodeCompleter();
  237. var dom = ace.require("ace/lib/dom");
  238. dom.importCssString(style);
  239. var snippetManager = ace.require("ace/snippets").snippetManager;
  240. var snippets = MODx.config['ace.snippets'] || '';
  241. snippetManager.register(snippetManager.parseSnippetFile(snippets), "_");
  242. var HashHandler = ace.require("ace/keyboard/hash_handler").HashHandler;
  243. var commands = new HashHandler();
  244. commands.addCommand({
  245. name: "insertsnippet",
  246. bindKey: {win: "Tab", mac: "Tab"},
  247. exec: function(editor) {
  248. return snippetManager.expandWithTab(editor);
  249. }
  250. });
  251. // to overwrite emmet
  252. var onChangeMode = function(e, target) {
  253. var editor = target;
  254. editor.keyBinding.addKeyboardHandler(commands);
  255. };
  256. onChangeMode({}, this.editor);
  257. var Emmet = ace.require("ace/ext/emmet");
  258. Emmet.isSupportedMode = function(modeId) {
  259. return modeId && /css|less|scss|sass|stylus|html|php|twig|ejs|handlebars|smarty/.test(modeId);
  260. };
  261. var net = ace.require('ace/lib/net');
  262. net.loadScript(MODx.config['assets_url'] + 'components/ace/emmet/emmet.js', function() {
  263. Emmet.setCore(window.emmet);
  264. this.editor.setOption("enableEmmet", true);
  265. this.editor.on("changeMode", onChangeMode);
  266. onChangeMode({}, this.editor);
  267. }.bind(this));
  268. ace.require('ace/ext/keybinding_menu').init(this.editor);
  269. MODx.ux.Ace.initialized = true;
  270. }
  271. this.editor.commands.addCommand({
  272. name: "showKeyboardShortcuts",
  273. bindKey: {win: "Ctrl-Alt-H", mac: "Command-Alt-H"},
  274. exec: function(editor) {
  275. editor.showKeyboardShortcuts();
  276. },
  277. readOnly: true
  278. });
  279. this.editor.commands.addCommand({
  280. name: "gotoline",
  281. bindKey: {win: "Ctrl-L", mac: "Command-Option-L"},
  282. exec: this.showGotoLineWindow.bind(this),
  283. readOnly: true
  284. });
  285. this.editor.commands.addCommand({
  286. name: "fullscreen",
  287. bindKey: {win: "Ctrl-F11", mac: "Command-F12"},
  288. exec: this.fullScreen.bind(this),
  289. readOnly: true
  290. });
  291. },
  292. fullScreen : function() {
  293. if (this.isFullscreen){
  294. this.maximizer.title = this.maximizeTitle;
  295. this.el.removeClass('ace_maximized');
  296. } else {
  297. this.el.addClass('ace_maximized');
  298. this.maximizer.title = this.minimizeTitle;
  299. }
  300. this.isFullscreen = !this.isFullscreen;
  301. this.onResize();
  302. },
  303. setMimeType : function (mimeType){
  304. this.setMode( MODx.ux.Ace.mimeTypes[mimeType] || 'text' );
  305. },
  306. showGotoLineWindow : function(){
  307. var window;
  308. if (!this.windows.gotoLine){
  309. this.windows.gotoLine = this.createGotoLineWindow();
  310. }
  311. window = this.windows.gotoLine;
  312. window.show();
  313. },
  314. doGotoLine : function(){
  315. var window, line;
  316. window = this.windows.gotoLine;
  317. line = window.fp.getForm().getFieldValues('line')['line'];
  318. if (!isNaN(line)){
  319. this.editor.gotoLine(line);
  320. window.hide();
  321. }
  322. },
  323. createGotoLineWindow: function () {
  324. var window = MODx.load({
  325. xtype: 'modx-window',
  326. title: _('ui_ace.goto_line')
  327. ,resizable: false
  328. ,maximizable: false
  329. ,allowDrop: false
  330. ,width: 300
  331. ,buttons: [{
  332. text: _('ui_ace.go')
  333. ,scope: this
  334. ,handler: this.doGotoLine
  335. },{
  336. text: _('ui_ace.close')
  337. ,scope: this
  338. ,handler: function() { window.hide(); }
  339. }]
  340. ,keys: [{
  341. key: Ext.EventObject.ENTER
  342. ,fn: this.doGotoLine
  343. ,scope: this
  344. }]
  345. ,action: 'gotoline'
  346. ,listeners: {
  347. 'hide': {fn: this.focus, scope: this}
  348. }
  349. ,fields: [{
  350. xtype: 'textfield'
  351. ,validator: function (value) {
  352. return !isNaN(value);
  353. }
  354. ,fieldLabel: _('ui_ace.goto_line')
  355. ,name: 'line'
  356. ,anchor: '100%'
  357. ,value: ''
  358. }]
  359. });
  360. return window;
  361. },
  362. setMode : function (mode){
  363. var editor = this.editor;
  364. if (!this.modxTags)
  365. return editor.session.setMode('ace/mode/' + mode);
  366. var config = ace.require('ace/config');
  367. config.loadModule(["mode", 'ace/mode/' + mode], function(module) {
  368. var mode = MODx.ux.Ace.createModxMixedMode(module.Mode);
  369. editor.session.setMode(mode);
  370. }.bind(this));
  371. }
  372. });
  373. MODx.ux.Ace.replaceComponent = function(id, mimeType, modxTags) {
  374. var textArea = Ext.getCmp(id);
  375. if (!textArea) {
  376. // Workaround for File Update panel (fix issue, caused by wrong event order)
  377. return setTimeout(function() {
  378. var textArea = Ext.getCmp(id);
  379. if (textArea)
  380. MODx.ux.Ace.replaceComponent(id, mimeType, modxTags);
  381. });
  382. }
  383. var textEditor = MODx.load({
  384. xtype: 'modx-texteditor',
  385. enableKeyEvents: true,
  386. anchor: textArea.anchor,
  387. width: 'auto',
  388. height: parseInt(MODx.config['ace.height']) || textArea.height,
  389. name: textArea.name,
  390. value: textArea.getValue(),
  391. mimeType: mimeType,
  392. modxTags: modxTags
  393. });
  394. textArea.el.dom.removeAttribute('name');
  395. textArea.el.setStyle('display', 'none');
  396. textEditor.render(textArea.el.dom.parentNode);
  397. textArea.setSize = function(){textEditor.setSize.apply(textEditor, arguments)};
  398. textEditor.editor.on('change', function(e){textArea.fireEvent('change', e);});
  399. textArea.on('destroy', function() {textEditor.destroy();});
  400. if (!modxTags)
  401. return;
  402. var dropTarget = MODx.load({
  403. xtype: 'modx-treedrop',
  404. target: textEditor,
  405. targetEl: textEditor.el,
  406. onInsert: (function(s){
  407. this.insertAtCursor(s);
  408. this.focus();
  409. return true;
  410. }).bind(textEditor),
  411. iframe: true
  412. });
  413. textArea.on('destroy', function() {dropTarget.destroy();});
  414. };
  415. MODx.ux.Ace.replaceTextAreas = function(textAreas, mimeType) {
  416. textAreas.forEach(function(textArea){
  417. var editor = MODx.load({
  418. xtype: 'modx-texteditor',
  419. width: 'auto',
  420. height: parseInt(textArea.style.height) || 200,
  421. name: textArea.name,
  422. value: textArea.value,
  423. mimeType: mimeType || 'text/html',
  424. modxTags: true
  425. });
  426. textArea.name = '';
  427. textArea.style.display = 'none';
  428. editor.render(textArea.parentNode);
  429. editor.editor.on('change', function(e){ MODx.fireResourceFormChange() });
  430. });
  431. };
  432. MODx.ux.Ace.createModxMixedMode = function(Mode) {
  433. function ModxMixedMode() {
  434. Mode.call(this);
  435. var HighlightRules = this.HighlightRules;
  436. function ModxMixedHighlightRules() {
  437. HighlightRules.call(this);
  438. this.$rules['modxtag-comment'] = [
  439. {
  440. token : "comment.modx",
  441. regex : "[^\\[\\]]+",
  442. merge : true
  443. },{
  444. token : "comment.modx",
  445. regex : "\\[\\[\\-.*?\\]\\]"
  446. },{
  447. token : "comment.modx",
  448. regex : "\\s+",
  449. merge : true
  450. },
  451. {
  452. token : "paren.rparen.comment.modx",
  453. regex : "\\]\\]",
  454. next: "pop"
  455. }
  456. ];
  457. this.$rules['modxtag-start'] = [
  458. {
  459. token : ["cache-flag.variable.modx", "tag-token.variable.modx", "tag-name.variable.modx"],
  460. regex : "(!)?([%|*|~|\\+|\\$]|(?:\\+\\+)|(?:\\*#))?([-_a-zA-Z0-9\\.]+)",
  461. push : [
  462. {include: "modxtag-filter"},
  463. {
  464. token: "tag-delimiter.keyword.operator.modx",
  465. regex: "\\?",
  466. push: [
  467. {token : "text.modx", regex : "\\s+"},
  468. {include: 'modxtag-property-string'},
  469. {token: "", regex: "$"},
  470. {token: '', regex: '', next: 'pop'}
  471. ]
  472. },
  473. {token : "text.modx", regex : "\\s+"},
  474. {token: "", regex: "$"},
  475. {token: '', regex: '', next: 'pop'}
  476. ]
  477. },
  478. {
  479. token : "support.constant.paren.lparen.modx", // opening tag
  480. regex : "\\[\\[",
  481. push : 'modxtag-start'
  482. },
  483. {
  484. token : "text",
  485. regex : "\\s+"
  486. },
  487. {
  488. token : "support.constant.paren.rparen.tag-brackets.modx",
  489. regex : "\\]\\]",
  490. next: "pop"
  491. },
  492. {defaultToken: 'text.modx'}
  493. ];
  494. this.$rules['modxtag-propertyset'] = [
  495. {
  496. token : ['keyword.operator.modx', "support.class.modx"],
  497. regex : "(@)([-_a-zA-Z0-9\\.]+|\\[\\[.*?\\]\\])",
  498. next : 'modxtag-filter'
  499. },
  500. {
  501. token : "text",
  502. regex : "\\s+"
  503. },
  504. {token: "", regex: "$"},
  505. {
  506. token: "empty",
  507. regex: "",
  508. next: "modxtag-filter"
  509. }
  510. ];
  511. this.$rules['modxtag-filter'] = [
  512. {
  513. token : 'filter-delimiter.keyword.operator.modx',
  514. regex : ":",
  515. push : [
  516. {
  517. token: "filter-name.support.function.modx",
  518. regex: "[-_a-zA-Z0-9]+|\\[\\[.*?\\]\\]",
  519. push: "modxtag-filter-eq"
  520. },
  521. {
  522. token: "empty",
  523. regex: "",
  524. next: "pop"
  525. }
  526. ]
  527. },
  528. {
  529. token : "text",
  530. regex : "\\s+"
  531. }
  532. ];
  533. this.$rules['modxtag-filter-eq'] = [
  534. {
  535. token : ["keyword.operator.modx"],
  536. regex : "="
  537. },{
  538. token : 'string',
  539. regex : '`',
  540. push: "modxtag-filter-value"
  541. },
  542. {
  543. token : "text",
  544. regex : "\\s+"
  545. },
  546. {
  547. token: "empty",
  548. regex: "",
  549. next: "pop"
  550. }
  551. ];
  552. this.$rules["modxtag-property-string"] = [
  553. {
  554. token : "entity.other.attribute-name.modx",
  555. regex: "&"
  556. },
  557. {
  558. token: "entity.other.attribute-name.modx",
  559. regex: "[-_a-zA-Z0-9]+"
  560. },
  561. {
  562. token : "string.modx",
  563. regex : '`',
  564. push : "modxtag-attribute-value"
  565. }, {
  566. token : "keyword.operator.modx",
  567. regex : "="
  568. }, {
  569. token : "entity.other.attribute-name.modx",
  570. regex : "[-_a-zA-Z0-9]+"
  571. },
  572. {
  573. token : "comment.modx",
  574. regex : "\\[\\[\\-.*?\\]\\]"
  575. },
  576. {
  577. token : "property-string.text.modx",
  578. regex : "\\s+"
  579. }
  580. ];
  581. this.$rules["modxtag-attribute-value"] = [
  582. {
  583. token : "string.modx",
  584. regex : "[^`\\[]+",
  585. merge : true
  586. },{
  587. token : "string.modx",
  588. regex : "[^`]+",
  589. merge : true
  590. },/* {
  591. token : "string",
  592. regex : "\\\\$",
  593. next : "modxtag-attribute-value",
  594. merge : true
  595. },*/ {
  596. token : "string.modx",
  597. regex : "`",
  598. next : "pop",
  599. merge : true
  600. }
  601. ];
  602. this.$rules["modxtag-filter-value"] = [
  603. {
  604. token : "string.modx",
  605. regex : "[^`\\[]+",
  606. merge : true
  607. },{
  608. token : "string.modx",
  609. regex : "\\[\\[.*?\\]\\]",
  610. merge : true
  611. }, {
  612. token : "string.modx",
  613. regex : "\\\\$",
  614. next : "pop",
  615. merge : true
  616. }, {
  617. token : "string.modx",
  618. regex : "`",
  619. next : "pop",
  620. merge : true
  621. }
  622. ];
  623. // add twig start tags to the HTML start tags
  624. for (var rule in this.$rules) {
  625. this.$rules[rule].unshift({
  626. token : "paren.lparen.comment.modx", // opening tag
  627. regex : "\\[\\[\\-",
  628. push : 'modxtag-comment',
  629. merge: true
  630. }, {
  631. token : "support.constant.paren.lparen.tag-brackets.modx", // opening tag
  632. regex : "\\[\\[",
  633. push : 'modxtag-start',
  634. merge : false
  635. });
  636. }
  637. this.normalizeRules();
  638. }
  639. ModxMixedHighlightRules.prototype = HighlightRules.prototype;
  640. this.HighlightRules = ModxMixedHighlightRules;
  641. if (typeof this.$behaviour == 'undefined') {
  642. var Behaviour = ace.require("ace/mode/behaviour").Behaviour;
  643. }
  644. this.$behaviour = Object.create(this.$behaviour || new Behaviour());
  645. this.$behaviour.add("brackets", "insertion", function (state, action, editor, session, text) {
  646. if (text == '[') {
  647. var selection = editor.getSelectionRange();
  648. var selected = session.doc.getTextRange(selection);
  649. if (selected !== "") {
  650. return {
  651. text: '[' + selected + ']',
  652. selection: false
  653. };
  654. } else {
  655. return {
  656. text: '[]',
  657. selection: [1, 1]
  658. };
  659. }
  660. } else if (text == ']') {
  661. var cursor = editor.getCursorPosition();
  662. var line = session.doc.getLine(cursor.row);
  663. var rightChar = line.substring(cursor.column, cursor.column + 1);
  664. if (rightChar == ']') {
  665. var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row});
  666. if (matching !== null) {
  667. return {
  668. text: '',
  669. selection: [1, 1]
  670. };
  671. }
  672. }
  673. }
  674. });
  675. this.$behaviour.add("brackets", "deletion", function (state, action, editor, session, range) {
  676. var selected = session.doc.getTextRange(range);
  677. if (!range.isMultiLine() && selected == '[') {
  678. var line = session.doc.getLine(range.start.row);
  679. var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
  680. if (rightChar == ']') {
  681. range.end.column++;
  682. return range;
  683. }
  684. }
  685. });
  686. this.$behaviour.add("string_apostrophes", "insertion", function (state, action, editor, session, text) {
  687. if (text == '`') {
  688. var quote = "`";
  689. var selection = editor.getSelectionRange();
  690. var selected = session.doc.getTextRange(selection);
  691. if (selected !== "") {
  692. return {
  693. text: quote + selected + quote,
  694. selection: false
  695. };
  696. } else {
  697. var cursor = editor.getCursorPosition();
  698. var line = session.doc.getLine(cursor.row);
  699. var leftChar = line.substring(cursor.column-1, cursor.column);
  700. // Find what token we're inside.
  701. var tokens = session.getTokens(selection.start.row);
  702. var col = 0, token;
  703. var quotepos = -1; // Track whether we're inside an open quote.
  704. for (var x = 0; x < tokens.length; x++) {
  705. token = tokens[x];
  706. if (token.type == "string.modx") {
  707. quotepos = -1;
  708. } else if (quotepos < 0) {
  709. quotepos = token.value.indexOf(quote);
  710. }
  711. if ((token.value.length + col) > selection.start.column) {
  712. break;
  713. }
  714. col += tokens[x].value.length;
  715. }
  716. // Try and be smart about when we auto insert.
  717. if (!token || (quotepos < 0 && token.type !== "comment" && (token.type !== "string.modx" || ((selection.start.column !== token.value.length+col-1) && token.value.lastIndexOf(quote) === token.value.length-1)))) {
  718. return {
  719. text: quote + quote,
  720. selection: [1,1]
  721. };
  722. } else if (token && token.type === "string.modx") {
  723. // Ignore input and move right one if we're typing over the closing quote.
  724. var rightChar = line.substring(cursor.column, cursor.column + 1);
  725. if (rightChar == quote) {
  726. return {
  727. text: '',
  728. selection: [1, 1]
  729. };
  730. }
  731. }
  732. }
  733. }
  734. });
  735. this.$behaviour.add("string_apostrophes", "deletion", function (state, action, editor, session, range) {
  736. var selected = session.doc.getTextRange(range);
  737. if (!range.isMultiLine() && (selected == '`')) {
  738. var line = session.doc.getLine(range.start.row);
  739. var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
  740. if (rightChar == '`') {
  741. range.end.column++;
  742. return range;
  743. }
  744. }
  745. });
  746. }
  747. ModxMixedMode.prototype = Object.create(Mode.prototype, {
  748. constructor: {value: ModxMixedMode}
  749. });
  750. return new ModxMixedMode();
  751. };
  752. MODx.ux.Ace.mimeTypes = {
  753. 'text/x-smarty' : 'smarty',
  754. 'text/html' : 'html',
  755. 'application/xhtml+xml' : 'html',
  756. 'text/css' : 'css',
  757. 'text/x-scss' : 'scss',
  758. 'text/x-less' : 'less',
  759. 'image/svg+xml' : 'svg',
  760. 'application/xml' : 'xml',
  761. 'text/xml' : 'xml',
  762. 'text/javascript' : 'javascript',
  763. 'application/javascript': 'javascript',
  764. 'application/json' : 'json',
  765. 'text/x-php' : 'php',
  766. 'application/x-php' : 'php',
  767. 'text/x-sql' : 'sql',
  768. 'text/x-markdown' : 'markdown',
  769. 'text/plain' : 'text',
  770. 'text/x-twig' : 'twig'
  771. };
  772. MODx.ux.Ace.initialized = false;
  773. MODx.ux.Ace.CodeCompleter = function() {
  774. var TokenIterator = ace.require("ace/token_iterator").TokenIterator;
  775. var langTools = ace.require("ace/ext/language_tools");
  776. var cache = {};
  777. function loadCompletions(params, callback) {
  778. Ext.Ajax.request({
  779. url: MODx.config.assets_url + 'components/ace/completions.php',
  780. params: params,
  781. success: function(response) {
  782. var completions = JSON.parse(response.responseText);
  783. callback(completions);
  784. }
  785. });
  786. }
  787. function gatherCompletions(completionParameters, callback) {
  788. var wait = 0;
  789. var completions = [];
  790. completionParameters.forEach(function(parameters){
  791. var data = cache[parameters.cacheKey];
  792. if (!data) {
  793. wait++;
  794. loadCompletions(parameters.requestParams, function(data) {
  795. wait--;
  796. cache[parameters.cacheKey] = data;
  797. completions = completions.concat(parameters.prepare(data));
  798. wait || callback(null, completions);
  799. });
  800. return;
  801. }
  802. completions = completions.concat(parameters.prepare(data));
  803. });
  804. wait || callback(null, completions);
  805. }
  806. function prepareCompletions(completions, meta) {
  807. return Object.keys(completions).map(function(completion){
  808. return {
  809. value: meta == 'chunk' ? '$' + completion : completion,
  810. caption: completion,
  811. meta: meta == 'function' ? completions[completion] : meta,
  812. description: completions[completion],
  813. score: 1000
  814. };
  815. });
  816. }
  817. function preparePropertyCompletions(completions) {
  818. return Object.keys(completions).map(function(completion){
  819. return {
  820. caption: completion,
  821. snippet: completion + '=`$0`',
  822. meta: 'property',
  823. description: completions[completion],
  824. score: 1000
  825. };
  826. });
  827. }
  828. function hasType(token, type) {
  829. var tokenTypes = token.type.split('.');
  830. return type.split('.').every(function(type){
  831. return (tokenTypes.indexOf(type) !== -1);
  832. });
  833. }
  834. function isWhitespace(string) {
  835. for (var i = 0; i < string.length; i++) {
  836. var c = string[i];
  837. if (!(c == ' ' || c == '\n' || c == '\r')) {
  838. return false;
  839. }
  840. }
  841. return true;
  842. }
  843. function parseTag(iterator) {
  844. var token = iterator.getCurrentToken();
  845. if (!token)
  846. return null;
  847. if (token.type.substring(token.type.lastIndexOf('.') + 1) !== 'modx')
  848. return null;
  849. while(token && hasType(token, 'text.modx') && isWhitespace(token.value))
  850. {
  851. token = iterator.stepBackward();
  852. }
  853. if (!token)
  854. return null;
  855. // we are in modx tag
  856. var completionType = 'object';
  857. var objectName = '';
  858. var classKey = 'modSnippet';
  859. if (hasType(token, 'tag-name')) {// [[*tag|]]
  860. objectName = token.value;
  861. token = iterator.stepBackward();
  862. }
  863. if (hasType(token, 'tag-brackets') && token.value == '[[') {// [[|]]
  864. classKey = 'modSnippet';
  865. } else if (hasType(token, 'cache-flag')) {
  866. //
  867. } else if (hasType(token, 'tag-token') || hasType(token, 'text')) {// [[*|]]
  868. switch (token.value) {
  869. case '$':
  870. classKey = 'modChunk';
  871. break;
  872. case '*':
  873. classKey = 'modTemplateVar';
  874. break;
  875. case '++':
  876. classKey = 'modSystemSetting';
  877. break;
  878. default:
  879. return null;
  880. }
  881. } else if (hasType(token, 'filter-name') || hasType(token, 'filter-delimiter')) {// [[*tag:filter|]], [[*tag:|]]
  882. completionType = 'filter';
  883. } else if (hasType(token, 'attribute-name') || hasType(token, 'tag-delimiter')) {// [[*tag?|]] , [[*tag? &prop|]]
  884. objectName = (function() {
  885. do {
  886. token = iterator.stepBackward();
  887. } while (token && !(hasType(token, 'tag-name') || hasType(token, 'modxtag-start')));
  888. if (token && hasType(token, 'tag-name'))
  889. return token.value;
  890. return null;
  891. })();
  892. if (!objectName)
  893. return null;
  894. completionType = 'property';
  895. } else {
  896. return null;
  897. }
  898. return {
  899. completionType: completionType,
  900. classKey: classKey,
  901. objectName: objectName,
  902. };
  903. }
  904. langTools.addCompleter({
  905. getCompletions: function(editor, session, pos, prefix, callback) {
  906. var iterator = new TokenIterator(session, pos.row, pos.column),
  907. parsedInfo = parseTag(iterator),
  908. completionType = 'function',
  909. classKey, objectName;
  910. if (parsedInfo) {
  911. completionType = parsedInfo.completionType;
  912. classKey = parsedInfo.classKey;
  913. objectName = parsedInfo.objectName;
  914. }
  915. switch (completionType) {
  916. case 'function' :
  917. gatherCompletions([
  918. {
  919. cacheKey: 'function',
  920. requestParams: {action: 'getFunctions'},
  921. prepare: function(completions) {
  922. return prepareCompletions(completions, 'function');
  923. }
  924. }
  925. ], callback);
  926. break;
  927. case 'propertyset':
  928. break;
  929. case 'lexiconentry':
  930. break;
  931. case 'property':
  932. gatherCompletions([
  933. {
  934. cacheKey: classKey + '.' + objectName,
  935. requestParams: {action: 'getProperties', classKey: classKey, key: objectName},
  936. prepare: function(completions) {
  937. return preparePropertyCompletions(completions, 'property');
  938. }
  939. }
  940. ], callback);
  941. break;
  942. case 'filter':
  943. gatherCompletions([
  944. {
  945. cacheKey: 'filter',
  946. requestParams: {action: 'getFilters'},
  947. prepare: function(completions) {
  948. return prepareCompletions(completions, 'filter');
  949. }
  950. }, {
  951. cacheKey: 'modSnippet',
  952. requestParams: {action: 'getObjects', classKey: 'modSnippet'},
  953. prepare: function(completions) {
  954. return prepareCompletions(completions, 'snippet');
  955. }
  956. },
  957. ], callback);
  958. break;
  959. case 'object':
  960. var aliases = {
  961. 'modSystemSetting': 'setting',
  962. 'modTemplateVar': 'tv',
  963. 'modSnippet': 'snippet',
  964. 'modChunk': 'chunk'
  965. };
  966. alias = aliases[classKey];
  967. var completionParameters = [];
  968. completionParameters[0] = {
  969. cacheKey: classKey,
  970. requestParams: {action: 'getObjects', classKey: classKey},
  971. prepare: function(completions) {
  972. return prepareCompletions(completions, alias);
  973. }
  974. };
  975. if (classKey == 'modTemplateVar') {
  976. completionParameters[1] = {
  977. cacheKey: 'resourcefield',
  978. requestParams: {action: 'getResourceFields'},
  979. prepare: function(completions) {
  980. return prepareCompletions(completions, 'field');
  981. }
  982. };
  983. }
  984. gatherCompletions(completionParameters, callback);
  985. break;
  986. }
  987. }
  988. });
  989. };
  990. Ext.reg('modx-texteditor',MODx.ux.Ace);