script.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. class ClassPage {
  2. static ModeCookieName = 'doczilla-js-docs-class-page-mode';
  3. static Mode = {
  4. Tabs: 'mode-tabs',
  5. List: 'mode-list'
  6. };
  7. static StyleClasses = {
  8. Selected: 'selected',
  9. Active: 'active',
  10. CtrlPressed: 'ctrl-pressed',
  11. ShiftPressed: 'shift-pressed',
  12. ParentsBranch: 'parents-branch',
  13. ClassItem: 'class-item',
  14. ClassName: 'class-name',
  15. ClassIcon: 'class-icon',
  16. Filler: 'filler',
  17. FullSourcePrompt: 'full-source-prompt',
  18. FullSourcePromptText: 'full-source-prompt-text',
  19. FullSourcePromptButton: 'full-source-prompt-button',
  20. Spacing: 'spacing',
  21. CmLink: 'cm-link',
  22. Z8Locale: 'z8-locale',
  23. Hidden: 'hidden',
  24. Collapsed: 'collapsed',
  25. Highlighted: 'highlighted',
  26. Readonly: 'readonly',
  27. Clickable: 'clickable',
  28. Empty: 'empty',
  29. White: 'white'
  30. };
  31. static Attributes = {
  32. DataTab: 'data-tab',
  33. DataDisplayMode: 'data-display-mode',
  34. DataClassName: 'data-class-name',
  35. OnClick: 'onclick',
  36. DataPropertyName: 'data-property-name',
  37. DataPropertyType: 'data-property-type',
  38. DataPropertyParent: 'data-property-parent',
  39. DataPropertyInherited: 'data-property-inherited',
  40. DataPropertyDynamic: 'data-property-dynamic'
  41. };
  42. static Messages = {
  43. NoMixins: 'This class has no mixins.',
  44. NoChildren: 'This class has no child classes.',
  45. NoMixedIn: 'This class is not mixed in any classes.',
  46. NoParents: 'This is a base class, which has no parent classes.',
  47. ShowFullSourceText: 'There were found another entities in the source file of this class. Would you like to see full source file?',
  48. HideFullSourceText: 'Full source file shown. Would you like to hide all entities except the target class?',
  49. PromptButtonText: 'OK',
  50. CmLinkTipPrefix: 'Ctrl+Click to go to class'
  51. };
  52. static TabNames = {
  53. Editor: 'Editor',
  54. Methods: 'Methods',
  55. Parents: 'Parents',
  56. Properties: 'Properties',
  57. Mixins: 'Mixins',
  58. Children: 'Children',
  59. MixedIn: 'MixedIn'
  60. };
  61. static PropertyType = {
  62. Statics: 'statics',
  63. Base: 'base',
  64. Overridden: 'overridden',
  65. Dynamic: 'dynamic',
  66. Inherited: 'inherited'
  67. };
  68. static PropertyLabel = {
  69. Statics: 'Static properties',
  70. Base: 'Base properties',
  71. Overridden: 'Overridden properties',
  72. Dynamic: 'Dynamic properties',
  73. Inherited: 'Inherited properties'
  74. };
  75. static MethodLabel = {
  76. Statics: 'Static methods',
  77. Base: 'Base methods',
  78. Overridden: 'Overridden methods',
  79. Dynamic: 'Dynamic methods',
  80. Inherited: 'Inherited methods'
  81. };
  82. static ClassProperties = {
  83. Name: 'name',
  84. Methods: 'methods',
  85. Properties: 'properties',
  86. Children: 'children',
  87. Mixins: 'mixins',
  88. MixedIn: 'mixedIn',
  89. ParentsBranch: 'parentsBranch',
  90. Statics: 'statics',
  91. DynamicProperties: 'dynamicProperties'
  92. };
  93. static ContextMenuType = {
  94. PropertyItem: 'property-item'
  95. };
  96. start() {
  97. if(typeof Class === 'string') {
  98. return this;
  99. }
  100. this.tabElements = {
  101. [ClassPage.TabNames.Editor]: DOM.get('.tab.editor'),
  102. [ClassPage.TabNames.Methods]: DOM.get('.tab.methods'),
  103. [ClassPage.TabNames.Parents]: DOM.get('.tab.parents'),
  104. [ClassPage.TabNames.Properties]: DOM.get('.tab.properties'),
  105. [ClassPage.TabNames.Mixins]: DOM.get('.tab.mixins'),
  106. [ClassPage.TabNames.Children]: DOM.get('.tab.children'),
  107. [ClassPage.TabNames.MixedIn]: DOM.get('.tab.mixedin')
  108. };
  109. this.contentElements = {
  110. [ClassPage.TabNames.Editor]: DOM.get('.content-tab#editor'),
  111. [ClassPage.TabNames.Methods]: DOM.get('.content-tab#methods'),
  112. [ClassPage.TabNames.Parents]: DOM.get('.content-tab#parents'),
  113. [ClassPage.TabNames.Properties]: DOM.get('.content-tab#properties'),
  114. [ClassPage.TabNames.Mixins]: DOM.get('.content-tab#mixins'),
  115. [ClassPage.TabNames.Children]: DOM.get('.content-tab#children'),
  116. [ClassPage.TabNames.MixedIn]: DOM.get('.content-tab#mixedin')
  117. };
  118. this.documented = Object.keys(Comments).length;
  119. this.documentable = 0;
  120. this.inheritedCommentsQuery = {};
  121. this.inheritedCommentsFields = {};
  122. this.documentedPercentage = DOM.get('.class-documented-percentage');
  123. const rightContainer = this.rightContainer = DOM.get('.right');
  124. const modeCookieValue = DOM.getCookieProperty(App.CookieName, ClassPage.ModeCookieName);
  125. if(!modeCookieValue)
  126. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, ClassPage.Mode.Tabs);
  127. const mode = this.mode = modeCookieValue || ClassPage.Mode.Tabs;
  128. const tabsModeButton = this.tabsModeButton = DOM.get('.display-mode-button.mode-tabs');
  129. const listModeButton = this.listModeButton = DOM.get('.display-mode-button.mode-list');
  130. /* >>> Context menu */
  131. this.contextMenu = DOM.get('.context-menu');
  132. this.contextMenuItems = {};
  133. /* <<< Context menu */
  134. (mode === ClassPage.Mode.Tabs ? tabsModeButton : listModeButton).addClass(ClassPage.StyleClasses.Selected);
  135. rightContainer.addClass(mode);
  136. this.renderContent();
  137. this.markContentInEditor();
  138. this.registerEventListeners();
  139. this.applyHash();
  140. return this;
  141. }
  142. switchMode(mode) {
  143. this.rightContainer.removeClass(this.mode);
  144. this.mode = mode;
  145. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, mode);
  146. this.rightContainer.addClass(mode);
  147. this.codeMirrorEditor.cmRefresh();
  148. }
  149. selectTab(tab) {
  150. tab = typeof tab === 'string' ? this.tabElements[tab] : tab;
  151. const selectedTab = this.selectedTab;
  152. let filler = selectedTab ? selectedTab.getFirstChild('.filler') : null;
  153. if(!filler)
  154. filler = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Filler });
  155. selectedTab && selectedTab.removeClass(ClassPage.StyleClasses.Selected);
  156. this.selectedTab = tab.addClass(ClassPage.StyleClasses.Selected);
  157. this.selectedTab.append(filler);
  158. }
  159. activateContent(content) {
  160. content = typeof content === 'string' ? this.contentElements[content] : content;
  161. if(this.activeContent)
  162. this.activeContent.removeClass(ClassPage.StyleClasses.Active);
  163. this.activeContent = content.addClass(ClassPage.StyleClasses.Active);
  164. if(content === this.contentElements[ClassPage.TabNames.Editor])
  165. this.codeMirrorEditor.cmRefresh();
  166. if(content === this.contentElements[ClassPage.TabNames.Properties] || content === this.contentElements[ClassPage.TabNames.Methods])
  167. DOM.getAll('.property-item-comment-input').forEach((item) => {
  168. item.style('height', `${Math.min(400, item.get().scrollHeight + 2)}px`);
  169. });
  170. }
  171. registerEventListeners() {
  172. this.registerTabsEventListeners();
  173. this.registerModeButtonsEventListeners();
  174. }
  175. registerTabsEventListeners() {
  176. const tabElements = this.tabElements;
  177. for(const tabName of Object.keys(tabElements)) {
  178. tabElements[tabName].on(DOM.Events.Click, this.onTabClick.bind(this));
  179. }
  180. }
  181. registerModeButtonsEventListeners() {
  182. this.tabsModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  183. this.listModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  184. }
  185. prepareSource() {
  186. const className = Class[ClassPage.ClassProperties.Name].replaceAll('.', '\\.');
  187. const classRx = new RegExp(`Z8\\.define\\(\'${className}\',\\s*\\{(?:.|[\r\n])+?^\\}\\);?`, 'gm');
  188. const classSourceMatch = ClassSource.match(classRx);
  189. if(!classSourceMatch) { // remove after `}\n);` issue fixed...
  190. this.sourceHasAnotherEntities = false;
  191. return this.classSource = ClassSource;
  192. }
  193. const classSource = this.classSource = classSourceMatch[0];
  194. this.sourceHasAnotherEntities = ClassSource.trim() !== classSource;
  195. return classSource;
  196. }
  197. renderContent() {
  198. this.renderEditor();
  199. this.renderParents();
  200. this.renderMixins();
  201. this.renderChildren();
  202. this.renderMixedIn();
  203. this.renderProperties();
  204. this.renderMethods();
  205. this.renderDocumentedPercentage();
  206. this.loadInheritedComments();
  207. }
  208. renderDocumentedPercentage() {
  209. this.documentedPercentage.setInnerHTML(`${Math.round(this.documented/this.documentable * 10000) / 100}%`);
  210. }
  211. renderEditor() {
  212. this.codeMirrorEditor = CodeMirror(DOM.get('#editor').get(), {
  213. [App.CodeMirrorProperties.Value]: this.prepareSource(ClassSource),
  214. [App.CodeMirrorProperties.Mode]: 'javascript',
  215. [App.CodeMirrorProperties.Theme]: 'darcula',
  216. [App.CodeMirrorProperties.Readonly]: true,
  217. [App.CodeMirrorProperties.LineNumbers]: true,
  218. [App.CodeMirrorProperties.MatchBrackets]: true,
  219. [App.CodeMirrorProperties.ScrollbarStyle]: 'overlay',
  220. [App.CodeMirrorProperties.ConfigureMouse]: (cm, repeat, ev) => {
  221. return { 'addNew': false };
  222. },
  223. });
  224. if(this.sourceHasAnotherEntities)
  225. this.renderFullSourcePrompt();
  226. this.codeMirrorEditorElement = DOM.get('.CodeMirror');
  227. }
  228. renderProperties() {
  229. const propertiesElement = this.contentElements[ClassPage.TabNames.Properties];
  230. const properties = this.getProperties(false);
  231. this.renderPropertiesType(properties, ClassPage.PropertyType.Statics, ClassPage.PropertyLabel.Statics, propertiesElement, false, false);
  232. this.renderPropertiesType(properties, ClassPage.PropertyType.Base, ClassPage.PropertyLabel.Base, propertiesElement, false, false);
  233. this.renderPropertiesType(properties, ClassPage.PropertyType.Overridden, ClassPage.PropertyLabel.Overridden, propertiesElement, false, false);
  234. this.renderPropertiesType(properties, ClassPage.PropertyType.Dynamic, ClassPage.PropertyLabel.Dynamic, propertiesElement, false, false);
  235. this.renderPropertiesType(properties, ClassPage.PropertyType.Inherited, ClassPage.PropertyLabel.Inherited, propertiesElement, true, false);
  236. }
  237. renderMethods() {
  238. const methodsElement = this.contentElements[ClassPage.TabNames.Methods];
  239. const properties = this.getProperties(true);
  240. this.renderPropertiesType(properties, ClassPage.PropertyType.Statics, ClassPage.MethodLabel.Statics, methodsElement, false, true);
  241. this.renderPropertiesType(properties, ClassPage.PropertyType.Base, ClassPage.MethodLabel.Base, methodsElement, false, true);
  242. this.renderPropertiesType(properties, ClassPage.PropertyType.Overridden, ClassPage.MethodLabel.Overridden, methodsElement, false, true);
  243. this.renderPropertiesType(properties, ClassPage.PropertyType.Dynamic, ClassPage.MethodLabel.Dynamic, methodsElement, false, true);
  244. this.renderPropertiesType(properties, ClassPage.PropertyType.Inherited, ClassPage.MethodLabel.Inherited, methodsElement, true, true);
  245. }
  246. renderPropertiesType(properties, type, headerText, container, initiallyHidden, isMethods) {
  247. properties = properties[type];
  248. if(!properties || properties.length == 0)
  249. return;
  250. const propertyItemClickable = (element) => {
  251. let el = element;
  252. while(!el.hasClass('property-item-comment-static') && !el.hasClass('property-item'))
  253. el = el.getParent();
  254. return element.hasClass('property-item-nearest-parent-span')
  255. || element.hasClass('property-item-comment-input')
  256. || element.hasClass('property-item-comment-button')
  257. || (el.hasClass('property-item-comment-static') && el.hasClass(ClassPage.StyleClasses.Clickable));
  258. };
  259. if(type !== ClassPage.PropertyType.Inherited) {
  260. this.documentable += properties.length;
  261. } else {
  262. const inheritedCommentsQuery = this.inheritedCommentsQuery;
  263. properties.forEach((prop) => {
  264. if(inheritedCommentsQuery[prop.nearestParent]) {
  265. inheritedCommentsQuery[prop.nearestParent].push(prop.key);
  266. } else {
  267. inheritedCommentsQuery[prop.nearestParent] = [prop.key];
  268. }
  269. });
  270. }
  271. const propertiesHeaderText = DOM.create({ tag: DOM.Tags.Span, cls: 'properties-header-text', innerHTML: headerText });
  272. const propertiesHeaderCollapsedIcon = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-header-collapsed-icon' });
  273. const propertiesHeader = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-header', cn: [propertiesHeaderText, propertiesHeaderCollapsedIcon] }, container);
  274. const propertiesList = DOM.create({ tag: DOM.Tags.Div, cls: 'properties-list' }, container);
  275. if(initiallyHidden) {
  276. propertiesList.addClass(ClassPage.StyleClasses.Hidden);
  277. propertiesHeader.addClass(ClassPage.StyleClasses.Collapsed);
  278. }
  279. for(const property of properties) {
  280. const propertyItem = DOM.create({ tag: DOM.Tags.Div, cls: 'property-item', attr: { [ClassPage.Attributes.DataPropertyName]: property.key, [ClassPage.Attributes.DataPropertyType]: property.type }}, propertiesList).on(DOM.Events.MouseDown, (e) => {
  281. if(e.buttons === DOM.MouseButtons.Right) {
  282. setTimeout(() => {
  283. this.showContextMenu(ClassPage.ContextMenuType.PropertyItem, CDElement.get(e.target), { x: e.pageX, y: e.pageY });
  284. }, 10);
  285. } else if (e.buttons === DOM.MouseButtons.Left) {
  286. const element = CDElement.get(e.target);
  287. if(propertyItemClickable(element))
  288. return;
  289. if(type === ClassPage.PropertyType.Inherited) {
  290. Url.goTo(`/class/${property.nearestParent}#${isMethods ? 'Methods' : 'Properties'}:${property.key}`);
  291. } else {
  292. this.searchPropertyInEditor(isMethods, property.dynamic, property.key);
  293. }
  294. }
  295. });
  296. const itemNameText = DOM.create({ tag: DOM.Tags.Span, innerHTML: isMethods ? 'Signature: ' : 'Name: ' });
  297. const itemNameValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-name-span', innerHTML: isMethods ? property.value : property.key });
  298. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-name', cn: [itemNameText, itemNameValue] }, propertyItem);
  299. if(type !== ClassPage.PropertyType.Dynamic && !isMethods) {
  300. const itemTypeText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Type: ' });
  301. const itemTypeValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-type-span', innerHTML: property.type });
  302. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-type', cn: [itemTypeText, itemTypeValue] }, propertyItem);
  303. }
  304. if(type !== ClassPage.PropertyType.Dynamic && !isMethods && property.type !== 'undefined') {
  305. const itemValueText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Default value: ' });
  306. const itemValueValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-default-value-span', innerHTML: property.type === 'string' ? `'${property.value}'` : property.value });
  307. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-default-value', cn: [itemValueText, itemValueValue] }, propertyItem);
  308. }
  309. if(type !== ClassPage.PropertyType.Dynamic && type !== ClassPage.PropertyType.Statics && type !== ClassPage.PropertyType.Base) {
  310. const itemParentText = DOM.create({ tag: DOM.Tags.Span, innerHTML: 'Nearest parent: ' });
  311. const itemParentValue = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-nearest-parent-span', innerHTML: property.nearestParent }).on('click', (e) => {
  312. Url.goTo(`/class/${property.nearestParent}`);
  313. });
  314. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-nearest-parent', cn: [itemParentText, itemParentValue] }, propertyItem);
  315. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyParent, property.nearestParent);
  316. }
  317. if(type === ClassPage.PropertyType.Dynamic) {
  318. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyDynamic, 'true');
  319. }
  320. const itemCommentText = DOM.create({ tag: DOM.Tags.Div, innerHTML: 'Comment:' });
  321. const itemCommentCn = [itemCommentText];
  322. const loadedComment = Comments[type === ClassPage.PropertyType.Statics ? `static:${property.key}` : property.key];
  323. const loadedCommentText = loadedComment && typeof loadedComment === 'object' ? loadedComment.comment : '';
  324. const hasComment = loadedComment && typeof loadedComment === 'object' && loadedCommentText.length > 0;
  325. const itemCommentStatic = DOM.create({ tag: DOM.Tags.Div, cls: `property-item-comment-static${!hasComment ? ' empty' : ''}`, innerHTML: hasComment ? CDUtils.nl2br(loadedCommentText) : 'Not commented yet...' });
  326. itemCommentCn.push(itemCommentStatic);
  327. if(type === ClassPage.PropertyType.Inherited) {
  328. this.inheritedCommentsFields[`${property.nearestParent}:${property.key}`] = itemCommentStatic;
  329. propertyItem.setAttribute(ClassPage.Attributes.DataPropertyInherited, 'true');
  330. }
  331. if(isAdmin) {
  332. const itemCommentInput = DOM.create({ tag: DOM.Tags.Textarea, cls: 'property-item-comment-input hidden', attr: { 'placeholder': 'Not commented yet...'} }).setValue(CDUtils.br2nl(loadedCommentText));
  333. itemCommentCn.push(itemCommentInput);
  334. if(type === ClassPage.PropertyType.Inherited) {
  335. itemCommentInput.addClass(ClassPage.StyleClasses.Readonly).setAttribute('readonly', 'true');
  336. } else {
  337. itemCommentStatic.addClass(ClassPage.StyleClasses.Clickable);
  338. itemCommentInput
  339. .on(DOM.Events.KeyDown, this.delayedAdjustCommentInputHeight.bind(this))
  340. .on(DOM.Events.Change, this.adjustCommentInputHeight.bind(this))
  341. .on(DOM.Events.Cut, this.delayedAdjustCommentInputHeight.bind(this))
  342. .on(DOM.Events.Paste, this.delayedAdjustCommentInputHeight.bind(this))
  343. .on(DOM.Events.Drop, this.delayedAdjustCommentInputHeight.bind(this));
  344. const onCommentSave = (e) => {
  345. const commentContent = itemCommentInput.getValue();
  346. const propertyName = `${type === ClassPage.PropertyType.Statics ? 'static:' : ''}${property.key}`;
  347. const className = Class[ClassPage.ClassProperties.Name];
  348. fetch('/updateComment', {
  349. method: 'POST',
  350. headers: {
  351. 'Content-Type': 'application/x-www-form-urlencoded'
  352. },
  353. body: new URLSearchParams({
  354. 'class': className,
  355. 'property': propertyName,
  356. 'comment': commentContent
  357. })
  358. }).then((res) => {
  359. if(res.status === 200 || res.status === 201) {
  360. if(res.status === 201) {
  361. this.documented++;
  362. this.renderDocumentedPercentage();
  363. propertyItem.append(this.createCommentDateElement(new Date()));
  364. } else {
  365. if(commentContent.length === 0) {
  366. this.documented--;
  367. this.renderDocumentedPercentage();
  368. propertyItem.getFirstChild('.property-item-comment-date').remove();
  369. } else {
  370. propertyItem.getFirstChild('.property-item-comment-date > .property-item-comment-date-date').setInnerHTML(CDUtils.dateFormatUTC(new Date(), 3, 'D.M.Y, H:I:S'));
  371. }
  372. }
  373. itemCommentStatic.setInnerHTML(commentContent.length > 0 ? CDUtils.nl2br(commentContent) : 'Not commented yet...');
  374. itemCommentStatic.switchClass(ClassPage.StyleClasses.Empty, commentContent.length === 0);
  375. } else {
  376. console.error('Comment update failed.');
  377. }
  378. itemCommentInput.switchClass(ClassPage.StyleClasses.Hidden);
  379. itemCommentOkButton.switchClass(ClassPage.StyleClasses.Hidden);
  380. itemCommentStatic.switchClass(ClassPage.StyleClasses.Hidden);
  381. });
  382. };
  383. const itemCommentOkButton = DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment-button hidden', innerHTML: 'OK' }).on(DOM.Events.Click, onCommentSave);
  384. itemCommentCn.push(itemCommentOkButton);
  385. itemCommentInput.on(DOM.Events.KeyDown, (e) => {
  386. if(e.key === DOM.Keys.Escape) {
  387. itemCommentInput.switchClass(ClassPage.StyleClasses.Hidden);
  388. itemCommentOkButton.switchClass(ClassPage.StyleClasses.Hidden);
  389. itemCommentStatic.switchClass(ClassPage.StyleClasses.Hidden);
  390. }
  391. if(e.key === DOM.Keys.Enter && !e.shiftKey) {
  392. onCommentSave();
  393. e.preventDefault();
  394. }
  395. });
  396. itemCommentStatic.on(DOM.Events.Click, (e) => {
  397. itemCommentInput.switchClass(ClassPage.StyleClasses.Hidden);
  398. itemCommentInput.focus();
  399. itemCommentOkButton.switchClass(ClassPage.StyleClasses.Hidden);
  400. itemCommentStatic.switchClass(ClassPage.StyleClasses.Hidden);
  401. });
  402. }
  403. }
  404. if(hasComment)
  405. itemCommentCn.push(this.createCommentDateElement(loadedComment.timestamp));
  406. DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment', cn: itemCommentCn }, propertyItem);
  407. }
  408. propertiesHeader.on(DOM.Events.Click, (e) => {
  409. propertiesList.switchClass(ClassPage.StyleClasses.Hidden);
  410. propertiesHeader.switchClass(ClassPage.StyleClasses.Collapsed);
  411. });
  412. }
  413. createCommentDateElement(date) {
  414. const commentDateText = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-comment-date-text', innerHTML: 'Commented on: ' });
  415. const commentDateDate = DOM.create({ tag: DOM.Tags.Span, cls: 'property-item-comment-date-date', innerHTML: CDUtils.dateFormatUTC(date, 3, 'D.M.Y, H:I:S') });
  416. return DOM.create({ tag: DOM.Tags.Div, cls: 'property-item-comment-date', cn: [commentDateText, commentDateDate] });
  417. }
  418. loadInheritedComments() {
  419. if(Object.keys(this.inheritedCommentsQuery).length === 0 || Object.keys(this.inheritedCommentsFields).length === 0)
  420. return;
  421. fetch('/getInheritedComments', {
  422. method: 'POST',
  423. headers: {
  424. 'Content-Type': 'application/x-www-form-urlencoded'
  425. },
  426. body: new URLSearchParams({
  427. query: JSON.stringify(this.inheritedCommentsQuery)
  428. })
  429. }).then(res => res.json()).then((inheritedComments) => {
  430. for(const cls of Object.keys(inheritedComments)) {
  431. const props = inheritedComments[cls];
  432. for(const prop of Object.keys(props)) {
  433. const element = this.inheritedCommentsFields[`${cls}:${prop}`];
  434. if(element) {
  435. element.setInnerHTML(props[prop].comment);
  436. element.removeClass(ClassPage.StyleClasses.Empty);
  437. element.getParent().append(this.createCommentDateElement(props[prop].timestamp));
  438. }
  439. }
  440. }
  441. });
  442. }
  443. getProperties(methods) {
  444. const filter = methods ? (item) => item.type === 'method' : (item) => item.type !== 'method';
  445. const statics = Class[ClassPage.ClassProperties.Statics].filter(filter);
  446. const properties = Class[ClassPage.ClassProperties.Properties].sort((a, b) => a.key.toLowerCase().localeCompare(b.key.toLowerCase())).sort((a, b) => {
  447. return Class[ClassPage.ClassProperties.ParentsBranch].indexOf(a.nearestParent) > Class[ClassPage.ClassProperties.ParentsBranch].indexOf(b.nearestParent) ? -1 : 1;
  448. }).filter(filter);
  449. const dynamicProperties = methods ? [] : Class[ClassPage.ClassProperties.DynamicProperties];
  450. const result = {
  451. [ClassPage.PropertyType.Statics]: statics,
  452. [ClassPage.PropertyType.Base]: properties.filter((item) => !item.inherited),
  453. [ClassPage.PropertyType.Overridden]: properties.filter((item) => item.overridden),
  454. [ClassPage.PropertyType.Inherited]: properties.filter((item) => item.inherited && !item.overridden),
  455. [ClassPage.PropertyType.Dynamic]: dynamicProperties
  456. };
  457. return result;
  458. }
  459. renderMixins() {
  460. const mixinsElement = this.contentElements[ClassPage.TabNames.Mixins];
  461. if(Class[ClassPage.ClassProperties.Mixins].length == 0) {
  462. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixins }, mixinsElement);
  463. mixinsElement.addClass(ClassPage.StyleClasses.Empty);
  464. return;
  465. }
  466. this.renderClassItems(Class[ClassPage.ClassProperties.Mixins], mixinsElement);
  467. }
  468. renderChildren() {
  469. const childrenElement = this.contentElements[ClassPage.TabNames.Children];
  470. if(Class[ClassPage.ClassProperties.Children].length == 0) {
  471. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoChildren }, childrenElement);
  472. childrenElement.addClass(ClassPage.StyleClasses.Empty);
  473. return;
  474. }
  475. this.renderClassItems(Class[ClassPage.ClassProperties.Children], childrenElement);
  476. }
  477. renderMixedIn() {
  478. const mixedInElement = this.contentElements[ClassPage.TabNames.MixedIn];
  479. if(Class[ClassPage.ClassProperties.MixedIn].length == 0) {
  480. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixedIn }, mixedInElement);
  481. mixedInElement.addClass(ClassPage.StyleClasses.Empty);
  482. return;
  483. }
  484. this.renderClassItems(Class[ClassPage.ClassProperties.MixedIn], mixedInElement);
  485. }
  486. renderFullSourcePrompt() {
  487. const editorContent = this.contentElements[ClassPage.TabNames.Editor];
  488. const prompt = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePrompt }, editorContent);
  489. const text = this.fullSourcePromptText = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptText, innerHTML: ClassPage.Messages.ShowFullSourceText }, prompt);
  490. const button = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptButton, innerHTML: ClassPage.Messages.PromptButtonText }, prompt);
  491. const onButtonClick = (e) => {
  492. this.switchFullSource();
  493. };
  494. button.on(DOM.Events.Click, onButtonClick.bind(this));
  495. }
  496. switchFullSource(show) {
  497. const shown = this.fullSourceCodeShown = show !== undefined ? !show : !this.fullSourceCodeShown;
  498. this.codeMirrorEditor.cmSetValue(shown ? ClassSource : this.classSource);
  499. if(shown)
  500. this.findAndScrollToTargetClass();
  501. this.codeMirrorEditor.cmRefresh();
  502. this.markContentInEditor();
  503. this.fullSourcePromptText && this.fullSourcePromptText.setInnerHTML(shown ? ClassPage.Messages.HideFullSourceText : ClassPage.Messages.ShowFullSourceText);
  504. return shown;
  505. }
  506. markContentInEditor() {
  507. this.codeMirrorEditor.cmEachLine((lineHandle) => {
  508. this.markExtend(lineHandle);
  509. this.markMixins(lineHandle);
  510. this.markZ8Locales(lineHandle);
  511. this.markNew(lineHandle);
  512. this.markThis(lineHandle);
  513. this.markProperties(lineHandle);
  514. });
  515. }
  516. findAndScrollToTargetClass() {
  517. const className = Class[ClassPage.ClassProperties.Name].replaceAll('.', '\\.');
  518. const editor = this.codeMirrorEditor;
  519. const defineRx = new RegExp(`Z8\\.define\\(\'${className}\',`);
  520. editor.cmEachLine((lineHandle) => {
  521. const text = lineHandle.text;
  522. const match = text.match(defineRx);
  523. if(match) {
  524. editor.scrollIntoView({ line: lineHandle.lineNo(), ch: 0 }, 100);
  525. return;
  526. }
  527. });
  528. }
  529. renderParents() {
  530. const parentsContent = this.contentElements[ClassPage.TabNames.Parents];
  531. const parentsContainer = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ParentsBranch }, parentsContent);
  532. if(Class[ClassPage.ClassProperties.ParentsBranch].length == 0) {
  533. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoParents }, parentsContent);
  534. parentsContent.addClass(ClassPage.StyleClasses.Empty);
  535. return;
  536. }
  537. this.renderClassItems(Class[ClassPage.ClassProperties.ParentsBranch], parentsContainer, true);
  538. }
  539. renderClassItems(itemsList, container, withIndent) {
  540. let indent = 0;
  541. if(!withIndent)
  542. itemsList = itemsList.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  543. for(const cls of itemsList) {
  544. const icon = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassIcon });
  545. const name = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassName, innerHTML: cls });
  546. const cn = [icon, name];
  547. if(indent > 0 && withIndent)
  548. cn.unshift(DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Spacing, style: `width: ${10 * indent}px;` }));
  549. DOM.create({ tag: DOM.Tags.Div, cls: `${ClassPage.StyleClasses.ClassItem}${ withIndent ? ' indent' : '' }`, attr: { [ClassPage.Attributes.DataClassName]: cls }, cn: cn }, container).on(DOM.Events.Click, this.onClassClick.bind(this));
  550. indent++;
  551. }
  552. }
  553. markExtend(lineHandle) {
  554. const editor = this.codeMirrorEditor;
  555. const text = lineHandle.text;
  556. const match = text.match(/extend:\s*['"]?([\w\.]+)['"]?/);
  557. if (match) {
  558. const className = match[1];
  559. const from = { line: lineHandle.lineNo(), ch: match.index + 8 };
  560. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  561. editor.markText(from, to, {
  562. className: ClassPage.StyleClasses.CmLink,
  563. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  564. attributes: {
  565. [ClassPage.Attributes.DataClassName]: className,
  566. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  567. }
  568. });
  569. }
  570. }
  571. markMixins(lineHandle) {
  572. const editor = this.codeMirrorEditor;
  573. const text = lineHandle.text;
  574. const match = text.match(/mixins:\s*(\[.*?\]|\w+)/);
  575. if (match) {
  576. let mixins = match[1].replace(/\[|\]/g, "").split(/\s*,\s*/);
  577. const mixinsStr = match[1].replace(/\[|\]/g, "");
  578. mixins = mixinsStr.split(/\s*,\s*/);
  579. const startIndex = match.index + match[0].indexOf(mixinsStr);
  580. for (var i = 0; i < mixins.length; i++) {
  581. const className = mixins[i].trim().replace(/^['"]|['"]$/g, "");
  582. const classIndex = mixinsStr.indexOf(className);
  583. const from = { line: lineHandle.lineNo(), ch: startIndex + classIndex };
  584. const to = { line: lineHandle.lineNo(), ch: startIndex + classIndex + className.length };
  585. editor.markText(from, to, {
  586. className: ClassPage.StyleClasses.CmLink,
  587. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  588. attributes: {
  589. [ClassPage.Attributes.DataClassName]: className,
  590. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  591. }
  592. });
  593. }
  594. }
  595. }
  596. markZ8Locales(lineHandle) {
  597. const editor = this.codeMirrorEditor;
  598. const text = lineHandle.text;
  599. const regexp = /Z8\.\$\('([\S]+)'(?:\s*,\s*.*)?\)/g;
  600. let match;
  601. while ((match = regexp.exec(text)) !== null) {
  602. const messageId = match[1];
  603. const from = { line: lineHandle.lineNo(), ch: match.index };
  604. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  605. editor.markText(from, to, {
  606. className: ClassPage.StyleClasses.Z8Locale,
  607. title: `RU: ${Z8Locales['ru'][messageId]}\nEN: ${Z8Locales['en'][messageId]}`
  608. });
  609. }
  610. }
  611. markNew(lineHandle) {
  612. const editor = this.codeMirrorEditor;
  613. const text = lineHandle.text;
  614. const regexp = /new\s+([\w\.]+)/g;
  615. let match;
  616. while ((match = regexp.exec(text)) !== null) {
  617. const className = match[1];
  618. const from = { line: lineHandle.lineNo(), ch: match.index + 4 };
  619. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  620. if(!ClassList[className] && !this.shortNameExists(className))
  621. continue;
  622. editor.markText(from, to, {
  623. className: ClassPage.StyleClasses.CmLink,
  624. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  625. attributes: {
  626. [ClassPage.Attributes.DataClassName]: className,
  627. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  628. }
  629. });
  630. }
  631. }
  632. markThis(lineHandle) {
  633. const editor = this.codeMirrorEditor;
  634. const text = lineHandle.text;
  635. const regexp = /this\.([\w]+)/g;
  636. let match;
  637. while ((match = regexp.exec(text)) !== null) {
  638. const propertyName = match[1];
  639. const from = { line: lineHandle.lineNo(), ch: match.index + 5 };
  640. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  641. const foundProperty = this.findClassProperty(propertyName);
  642. if(!foundProperty)
  643. continue;
  644. editor.markText(from, to, {
  645. className: 'cm-this-prop',
  646. title: `Ctrl+Click to go to ${foundProperty.type === 'method' ? 'method' : 'property'} '${propertyName}'`,
  647. attributes: {
  648. [ClassPage.Attributes.DataPropertyName]: propertyName,
  649. [ClassPage.Attributes.DataPropertyType]: foundProperty.type === 'method' ? 'Methods' : 'Properties',
  650. [ClassPage.Attributes.DataPropertyParent]: foundProperty.inherited ? foundProperty.nearestParent : '',
  651. [ClassPage.Attributes.OnClick]: 'window.page.onPropertyClick(this);'
  652. }
  653. });
  654. }
  655. }
  656. markProperties(lineHandle) {
  657. const editor = this.codeMirrorEditor;
  658. const text = lineHandle.text;
  659. const regexp = /\t([\w]+):/g;
  660. let match;
  661. while ((match = regexp.exec(text)) !== null) {
  662. const propertyName = match[1];
  663. const from = { line: lineHandle.lineNo(), ch: match.index + 1 };
  664. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length - 1 };
  665. const foundProperty = this.findClassProperty(propertyName);
  666. if(!foundProperty)
  667. continue;
  668. editor.markText(from, to, {
  669. className: 'cm-this-prop',
  670. title: `Ctrl+Click to go to ${foundProperty.type === 'method' ? 'method' : 'property'} '${propertyName}'`,
  671. attributes: {
  672. [ClassPage.Attributes.DataPropertyName]: propertyName,
  673. [ClassPage.Attributes.DataPropertyType]: foundProperty.type === 'method' ? 'Methods' : 'Properties',
  674. [ClassPage.Attributes.DataPropertyParent]: foundProperty.inherited ? foundProperty.nearestParent : '',
  675. [ClassPage.Attributes.OnClick]: 'window.page.onPropertyClick(this);'
  676. }
  677. });
  678. }
  679. }
  680. shortNameExists(shortName) {
  681. return Object.keys(ClassList).map((key) => ClassList[key]).filter((item) => item.shortName === shortName).length > 0;
  682. }
  683. findClassProperty(propertyName) {
  684. const dynamicProperties = Class[ClassPage.ClassProperties.DynamicProperties];
  685. const properties = Class[ClassPage.ClassProperties.Properties];
  686. const statics = Class[ClassPage.ClassProperties.Statics];
  687. const foundStatic = statics.filter((prop) => prop.key === propertyName)[0];
  688. const foundDynamic = dynamicProperties.filter((prop) => prop.key === propertyName)[0];
  689. const foundProperty = properties.filter((prop) => prop.key === propertyName)[0];
  690. return foundStatic || foundDynamic || foundProperty;
  691. }
  692. onClassLinkClick(fragment) {
  693. const codeMirrorEditorElement = this.codeMirrorEditorElement;
  694. if(codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed))
  695. Url.goTo(`/class/${fragment.getAttribute(ClassPage.Attributes.DataClassName)}`, codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.ShiftPressed));
  696. }
  697. onPropertyClick(fragment) {
  698. const codeMirrorEditorElement = this.codeMirrorEditorElement;
  699. if(codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed)) {
  700. const parentClassName = fragment.getAttribute(ClassPage.Attributes.DataPropertyParent);
  701. const propertyType = fragment.getAttribute(ClassPage.Attributes.DataPropertyType);
  702. const propertyName = fragment.getAttribute(ClassPage.Attributes.DataPropertyName);
  703. if(parentClassName.length > 0)
  704. Url.goTo(`/class/${parentClassName}#${propertyType}:${propertyName}`);
  705. else
  706. Url.setHash(`${propertyType}:${propertyName}`).updateLocation();
  707. }
  708. }
  709. onTabClick(e) {
  710. const element = CDElement.get(e.target);
  711. if(element.hasClass(ClassPage.StyleClasses.Selected))
  712. return;
  713. this.openTab(element.getAttribute(ClassPage.Attributes.DataTab));
  714. }
  715. openTab(tabName) {
  716. this.selectTab(tabName);
  717. this.activateContent(tabName);
  718. Url.setHash(tabName).updateLocation();
  719. }
  720. onClassClick(e) {
  721. let element = CDElement.get(e.target);
  722. while(!element.hasClass(ClassPage.StyleClasses.ClassItem))
  723. element = element.getParent();
  724. Url.goTo(`/class/${element.getAttribute(ClassPage.Attributes.DataClassName)}`);
  725. }
  726. onModeButtonClick(e) {
  727. const button = CDElement.get(e.target);
  728. const mode = button.getAttribute(ClassPage.Attributes.DataDisplayMode);
  729. this.switchMode(mode);
  730. button.addClass(ClassPage.StyleClasses.Selected);
  731. (mode === ClassPage.Mode.Tabs ? this.listModeButton : this.tabsModeButton).removeClass(ClassPage.StyleClasses.Selected);
  732. }
  733. searchInEditor(...queries) {
  734. const editor = this.codeMirrorEditor;
  735. for(const query of queries) {
  736. const cursor = editor.getSearchCursor(query, CodeMirror.Pos(editor.cmFirstLine(), 0), { caseFold: false, multiline: true });
  737. if(cursor.find(false)) {
  738. this.switchFullSource(true);
  739. this.openTab('Editor');
  740. editor.setSelection(cursor.from(), cursor.to());
  741. editor.scrollIntoView({from: cursor.from(), to: cursor.to()}, 100);
  742. return;
  743. }
  744. }
  745. }
  746. searchPropertyInEditor(isMethod, isDynamic, propertyName) {
  747. if(isMethod) {
  748. this.searchInEditor(`${propertyName}: function`);
  749. } else {
  750. if(isDynamic)
  751. this.searchInEditor(`this.${propertyName} =`, `this.${propertyName}`);
  752. else
  753. this.searchInEditor(`${propertyName}: `);
  754. }
  755. }
  756. scrollToProperty(hashTab, hashProp) {
  757. if(!hashProp)
  758. return;
  759. const item = this.contentElements[hashTab].getFirstChild(`.property-item[data-property-name="${hashProp}"]`);
  760. if(!item)
  761. return;
  762. const categoryList = item.getParent();
  763. const categoryHeader = categoryList.previousSibling();
  764. if(categoryList.hasClass(ClassPage.StyleClasses.Hidden))
  765. categoryList.removeClass(ClassPage.StyleClasses.Hidden);
  766. if(categoryHeader.hasClass(ClassPage.StyleClasses.Collapsed))
  767. categoryHeader.removeClass(ClassPage.StyleClasses.Collapsed);
  768. item.addClass(ClassPage.StyleClasses.Highlighted);
  769. item.addClass(ClassPage.StyleClasses.White);
  770. if(this.mode === ClassPage.Mode.Tabs)
  771. this.contentElements[hashTab].scrollTo(`.property-item[data-property-name="${hashProp}"]`);
  772. else
  773. item.scrollIntoView();
  774. setTimeout(() => {
  775. item.removeClass(ClassPage.StyleClasses.White);
  776. }, 1000);
  777. setTimeout(() => {
  778. item.removeClass(ClassPage.StyleClasses.Highlighted);
  779. }, 2000);
  780. }
  781. delayedAdjustCommentInputHeight(e) {
  782. setTimeout(() => { this.adjustCommentInputHeight(e) }, 0);
  783. }
  784. adjustCommentInputHeight(e) {
  785. const textArea = e.target;
  786. if(textArea.scrollHeight < 400 || e.key === DOM.Keys.Backspace) {
  787. textArea.style.height = 'auto';
  788. textArea.style.height = `${Math.min(400, textArea.scrollHeight + 2)}px`;
  789. }
  790. textArea.scrollTop = textArea.scrollHeight;
  791. }
  792. applyHash() {
  793. const hash = (Url.getHash() || '').split(':');
  794. const hashTab = hash[0];
  795. const hashProp = hash[1];
  796. const tabElements = this.tabElements;
  797. const contentElements = this.contentElements;
  798. const selectedTab = tabElements[hashTab] || tabElements[ClassPage.TabNames.Editor];
  799. const activeContent = contentElements[hashTab] || contentElements[ClassPage.TabNames.Editor];
  800. this.selectTab(selectedTab);
  801. this.activateContent(activeContent);
  802. this.scrollToProperty(hashTab, hashProp);
  803. }
  804. /* >>> Context menu | TODO: move to a completely independent module? */
  805. showContextMenu(contextMenuType, target, pos) {
  806. while(!target.hasClass(contextMenuType))
  807. target = target.getParent();
  808. switch(contextMenuType) {
  809. case ClassPage.ContextMenuType.PropertyItem:
  810. const propertyItemName = target.getAttribute(ClassPage.Attributes.DataPropertyName);
  811. const propertyItemType = target.getAttribute(ClassPage.Attributes.DataPropertyType);
  812. const propertyItemParent = target.getAttribute(ClassPage.Attributes.DataPropertyParent);
  813. const propertyItemDynamic = target.getAttribute(ClassPage.Attributes.DataPropertyDynamic);
  814. const propertyItemInhertied = target.getAttribute(ClassPage.Attributes.DataPropertyInherited);
  815. if(propertyItemInhertied !== 'true') {
  816. this.createContextMenuItem('ShowInEditor', 'Show in Editor', () => {
  817. this.searchPropertyInEditor(propertyItemType === 'method', propertyItemDynamic === 'true', propertyItemName);
  818. });
  819. }
  820. if(propertyItemParent != null) {
  821. this.createContextMenuItem('MoveToParent', 'Move to parent', () => {
  822. Url.goTo(`/class/${propertyItemParent}#${propertyItemType === 'method' ? 'Methods' : 'Properties'}:${propertyItemName}`);
  823. });
  824. }
  825. if(isAdmin && !target.getFirstChild('.property-item-comment-static').hasClass(ClassPage.StyleClasses.Hidden)) {
  826. this.createContextMenuDelimiter();
  827. this.createContextMenuItem('EditComment', 'Edit comment', () => {
  828. target.getFirstChild('.property-item-comment-static').click();
  829. target.getFirstChild('.property-item-comment-input').focus();
  830. });
  831. }
  832. this.createContextMenuDelimiter();
  833. this.createContextMenuItem('CopyLink', 'Copy link', () => {
  834. DOM.copyToClipboard(`${Url.getFullPath()}#${propertyItemType === 'method' ? 'Methods' : 'Properties'}:${propertyItemName}`);
  835. });
  836. break;
  837. }
  838. this.contextMenu.style('left', `${pos.x}px`).style('top', `${pos.y}px`);
  839. this.contextMenu.removeClass(ClassPage.StyleClasses.Hidden);
  840. }
  841. createContextMenuItem(name, text, action) {
  842. const itemAction = (e) => {
  843. action(e);
  844. this.hideContextMenu();
  845. };
  846. const item = DOM.create({ tag: DOM.Tags.Div, cls: 'context-menu-item', innerHTML: text, attr: { 'data-context-menu-item-name': name } }, this.contextMenu)
  847. .on(DOM.Events.Click, itemAction);
  848. this.contextMenuItems[name] = { item: item, action: itemAction };
  849. }
  850. createContextMenuDelimiter() {
  851. DOM.create({ tag: DOM.Tags.Div, cls: 'context-menu-delimiter' }, this.contextMenu);
  852. }
  853. clearContextMenu() {
  854. for(const item of this.contextMenu.getChildren()) {
  855. const name = item.getAttribute('data-context-menu-item-name');
  856. if(name) {
  857. const action = this.contextMenuItems[name].action;
  858. item.un(DOM.Events.Click, action);
  859. this.contextMenuItems[name] = null;
  860. delete this.contextMenuItems[name];
  861. }
  862. item.remove();
  863. }
  864. }
  865. hideContextMenu() {
  866. this.contextMenu.addClass(ClassPage.StyleClasses.Hidden);
  867. this.clearContextMenu();
  868. }
  869. /* <<< Context menu */
  870. };
  871. window_.on(DOM.Events.Load, (e) => {
  872. window.page = new ClassPage().start();
  873. });
  874. window_.on(DOM.Events.KeyDown, (e) => {
  875. if(window.page && e.key === DOM.Keys.Control)
  876. window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.CtrlPressed);
  877. if(window.page && e.key === DOM.Keys.Shift)
  878. window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.ShiftPressed);
  879. });
  880. window_.on(DOM.Events.KeyUp, (e) => {
  881. if(window.page && e.key === DOM.Keys.Control)
  882. window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.CtrlPressed);
  883. if(window.page && e.key === DOM.Keys.Shift)
  884. window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.ShiftPressed);
  885. });
  886. window_.on(DOM.Events.HashChange, (e) => {
  887. if(window.page)
  888. window.page.applyHash();
  889. });
  890. window_.on(DOM.Events.MouseDown, (e) => {
  891. if(window.page) {
  892. let target = CDElement.get(e.target);
  893. while(target != null && !target.hasClass('context-menu')) {
  894. target = target.getParent();
  895. }
  896. if(target != null && target.hasClass('context-menu'))
  897. return;
  898. if(!window.page.contextMenu.hasClass(ClassPage.StyleClasses.Hidden))
  899. window.page.hideContextMenu();
  900. }
  901. });