script.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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. ParentsBranch: 'parents-branch',
  12. ClassItem: 'class-item',
  13. ClassName: 'class-name',
  14. ClassIcon: 'class-icon',
  15. Filler: 'filler',
  16. FullSourcePrompt: 'full-source-prompt',
  17. FullSourcePromptText: 'full-source-prompt-text',
  18. FullSourcePromptButton: 'full-source-prompt-button',
  19. Spacing: 'spacing',
  20. CmLink: 'cm-link',
  21. Hidden: 'hidden'
  22. };
  23. static Attributes = {
  24. DataTab: 'data-tab',
  25. DataDisplayMode: 'data-display-mode',
  26. DataClassName: 'data-class-name',
  27. OnClick: 'onclick'
  28. };
  29. static Messages = {
  30. NoMixins: 'This class has no mixins.',
  31. NoChildren: 'This class has no child classes.',
  32. NoMixedIn: 'This class is not mixed in any classes.',
  33. NoParents: 'This is a base class, which has no parent classes.',
  34. ShowFullSourceText: 'There were found another entities in the source file of this class. Would you like to see full source file?',
  35. HideFullSourceText: 'Full source file shown. Would you like to hide all entities except the target class?',
  36. PromptButtonText: 'OK',
  37. CmLinkTipPrefix: 'Ctrl+Click to go to class'
  38. };
  39. start() {
  40. const tabElements = this.tabElements = {
  41. Editor: DOM.get('.tab.editor'),
  42. Methods: DOM.get('.tab.methods'),
  43. Parents: DOM.get('.tab.parents'),
  44. Properties: DOM.get('.tab.properties'),
  45. Mixins: DOM.get('.tab.mixins'),
  46. Children: DOM.get('.tab.children'),
  47. MixedIn: DOM.get('.tab.mixedin')
  48. };
  49. const contentElements = this.contentElements = {
  50. Editor: DOM.get('.content-tab#editor'),
  51. Methods: DOM.get('.content-tab#methods'),
  52. Parents: DOM.get('.content-tab#parents'),
  53. Properties: DOM.get('.content-tab#properties'),
  54. Mixins: DOM.get('.content-tab#mixins'),
  55. Children: DOM.get('.content-tab#children'),
  56. MixedIn: DOM.get('.content-tab#mixedin')
  57. };
  58. const rightContainer = this.rightContainer = DOM.get('.right');
  59. const selectedTab = this.selectedTab = tabElements[Url.getHash()] || tabElements.Editor;
  60. const activeContent = this.activeContent = contentElements[Url.getHash()] || contentElements.Editor;
  61. const modeCookieValue = DOM.getCookieProperty(App.CookieName, ClassPage.ModeCookieName);
  62. if(!modeCookieValue)
  63. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, ClassPage.Mode.Tabs);
  64. const mode = this.mode = modeCookieValue || ClassPage.Mode.Tabs;
  65. const tabsModeButton = this.tabsModeButton = DOM.get('.display-mode-button.mode-tabs');
  66. const listModeButton = this.listModeButton = DOM.get('.display-mode-button.mode-list');
  67. (mode === ClassPage.Mode.Tabs ? tabsModeButton : listModeButton).addClass(ClassPage.StyleClasses.Selected);
  68. rightContainer.addClass(mode);
  69. this.renderContent();
  70. this.selectTab(selectedTab);
  71. this.activateContent(activeContent);
  72. this.codeMirrorEditor.eachLine((lineHandle) => {
  73. this.markExtend(lineHandle);
  74. this.markMixins(lineHandle);
  75. });
  76. this.registerEventListeners();
  77. return this;
  78. }
  79. switchMode(mode) {
  80. this.rightContainer.removeClass(this.mode);
  81. this.mode = mode;
  82. DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, mode);
  83. this.rightContainer.addClass(mode);
  84. this.codeMirrorEditor.refresh();
  85. }
  86. selectTab(tab) {
  87. tab = typeof tab === 'string' ? this.tabElements[tab] : tab;
  88. const selectedTab = this.selectedTab;
  89. let filler = selectedTab.getFirstChild('.filler');
  90. if(!filler)
  91. filler = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Filler });
  92. selectedTab.removeClass(ClassPage.StyleClasses.Selected);
  93. this.selectedTab = tab.addClass(ClassPage.StyleClasses.Selected);
  94. this.selectedTab.append(filler);
  95. }
  96. activateContent(content) {
  97. content = typeof content === 'string' ? this.contentElements[content] : content;
  98. this.activeContent.removeClass(ClassPage.StyleClasses.Active);
  99. this.activeContent = content.addClass(ClassPage.StyleClasses.Active);
  100. if(content === this.contentElements.Editor)
  101. this.codeMirrorEditor.refresh();
  102. }
  103. registerEventListeners() {
  104. this.registerTabsEventListeners();
  105. this.registerModeButtonsEventListeners();
  106. }
  107. registerTabsEventListeners() {
  108. const tabElements = this.tabElements;
  109. for(const tabName of Object.keys(tabElements)) {
  110. tabElements[tabName].on(DOM.Events.Click, this.onTabClick.bind(this));
  111. }
  112. }
  113. registerModeButtonsEventListeners() {
  114. this.tabsModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  115. this.listModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this));
  116. }
  117. prepareSource() {
  118. const className = Class.name.replaceAll('.', '\\.');
  119. const classRx = new RegExp(`Z8\\.define\\(\'${className}\',\\s*\\{(?:.|[\r\n])+?^\\}\\);?`, 'gm');
  120. const classSource = this.classSource = ClassSource.match(classRx)[0];
  121. this.sourceHasAnotherEntities = ClassSource.trim() !== classSource;
  122. return classSource;
  123. }
  124. renderContent() {
  125. this.renderEditor();
  126. this.renderParents();
  127. this.renderMixins();
  128. this.renderChildren();
  129. this.renderMixedIn();
  130. }
  131. renderEditor() {
  132. this.codeMirrorEditor = CodeMirror(DOM.get('#editor').get(), {
  133. value: this.prepareSource(ClassSource),
  134. mode: 'javascript',
  135. theme: 'darcula',
  136. readOnly: true,
  137. lineNumbers: true,
  138. matchBrackets: true,
  139. scrollbarStyle: 'overlay'
  140. });
  141. if(this.sourceHasAnotherEntities)
  142. this.renderFullSourcePrompt();
  143. this.codeMirrorEditorElement = DOM.get('.CodeMirror');
  144. }
  145. renderMixins() {
  146. const mixinsElement = this.contentElements.Mixins;
  147. if(Class.mixins.length == 0) {
  148. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixins }, mixinsElement);
  149. return;
  150. }
  151. this.renderClassItems(Class.mixins, mixinsElement);
  152. }
  153. renderChildren() {
  154. const childrenElement = this.contentElements.Children;
  155. if(Class.children.length == 0) {
  156. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoChildren }, childrenElement);
  157. return;
  158. }
  159. this.renderClassItems(Class.children, childrenElement);
  160. }
  161. renderMixedIn() {
  162. const mixedInElement = this.contentElements.MixedIn;
  163. if(Class.mixedIn.length == 0) {
  164. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixedIn }, mixedInElement);
  165. return;
  166. }
  167. this.renderClassItems(Class.mixedIn, mixedInElement);
  168. }
  169. renderFullSourcePrompt() {
  170. const editorContent = this.contentElements.Editor;
  171. const prompt = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePrompt }, editorContent);
  172. const text = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptText, innerHTML: ClassPage.Messages.ShowFullSourceText }, prompt);
  173. const button = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptButton, innerHTML: ClassPage.Messages.PromptButtonText }, prompt);
  174. const onButtonClick = (e) => {
  175. text.setInnerHTML(this.switchFullSource() ? ClassPage.Messages.HideFullSourceText : ClassPage.Messages.ShowFullSourceText);
  176. };
  177. button.on(DOM.Events.Click, onButtonClick.bind(this));
  178. }
  179. switchFullSource() {
  180. const show = this.fullSourceCodeShown = !this.fullSourceCodeShown;
  181. this.codeMirrorEditor.setValue(show ? ClassSource : this.classSource);
  182. if(show)
  183. this.findAndScrollToTargetClass();
  184. this.codeMirrorEditor.refresh();
  185. return show;
  186. }
  187. findAndScrollToTargetClass() {
  188. const className = Class.name.replaceAll('.', '\\.');
  189. const editor = this.codeMirrorEditor;
  190. const defineRx = new RegExp(`Z8\\.define\\(\'${className}\',`);
  191. editor.eachLine((lineHandle) => {
  192. const text = lineHandle.text;
  193. const match = text.match(defineRx);
  194. if(match) {
  195. editor.scrollIntoView({ line: lineHandle.lineNo(), ch: 0 });
  196. return;
  197. }
  198. });
  199. }
  200. renderParents() {
  201. const parentsContent = this.contentElements.Parents;
  202. const parentsContainer = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ParentsBranch }, parentsContent);
  203. if(Class.parentsBranch.length == 0) {
  204. DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoParents }, parentsContent);
  205. return;
  206. }
  207. this.renderClassItems(Class.parentsBranch, parentsContainer, true);
  208. }
  209. renderClassItems(itemsList, container, withIndent) {
  210. let indent = 0;
  211. if(!withIndent)
  212. itemsList = itemsList.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  213. for(const cls of itemsList) {
  214. const icon = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassIcon });
  215. const name = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassName, innerHTML: cls });
  216. const cn = [icon, name];
  217. if(indent > 0 && withIndent)
  218. cn.unshift(DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Spacing, style: `width: ${10 * indent}px;` }));
  219. 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));
  220. indent++;
  221. }
  222. }
  223. markExtend(lineHandle) {
  224. const editor = this.codeMirrorEditor;
  225. const text = lineHandle.text;
  226. const match = text.match(/extend:\s*['"]?([\w\.]+)['"]?/);
  227. if (match) {
  228. const className = match[1];
  229. const from = { line: lineHandle.lineNo(), ch: match.index + 8 };
  230. const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length };
  231. editor.markText(from, to, {
  232. className: ClassPage.StyleClasses.CmLink,
  233. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  234. attributes: {
  235. [ClassPage.Attributes.DataClassName]: className,
  236. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  237. }
  238. });
  239. }
  240. }
  241. markMixins(lineHandle) {
  242. const editor = this.codeMirrorEditor;
  243. const text = lineHandle.text;
  244. const match = text.match(/mixins:\s*(\[.*?\]|\w+)/);
  245. if (match) {
  246. let mixins = match[1].replace(/\[|\]/g, "").split(/\s*,\s*/);
  247. const mixinsStr = match[1].replace(/\[|\]/g, "");
  248. mixins = mixinsStr.split(/\s*,\s*/);
  249. const startIndex = match.index + match[0].indexOf(mixinsStr);
  250. for (var i = 0; i < mixins.length; i++) {
  251. const className = mixins[i].trim().replace(/^['"]|['"]$/g, "");
  252. const classIndex = mixinsStr.indexOf(className);
  253. const from = { line: lineHandle.lineNo(), ch: startIndex + classIndex };
  254. const to = { line: lineHandle.lineNo(), ch: startIndex + classIndex + className.length };
  255. editor.markText(from, to, {
  256. className: ClassPage.StyleClasses.CmLink,
  257. title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`,
  258. attributes: {
  259. [ClassPage.Attributes.DataClassName]: className,
  260. [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);'
  261. }
  262. });
  263. }
  264. }
  265. }
  266. onClassLinkClick(fragment) {
  267. if(this.codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed))
  268. Url.goTo(`/class/${fragment.getAttribute(ClassPage.Attributes.DataClassName)}`);
  269. }
  270. onTabClick(e) {
  271. const element = CDElement.get(e.target);
  272. if(element.hasClass(ClassPage.StyleClasses.Selected))
  273. return;
  274. const tabName = element.getAttribute(ClassPage.Attributes.DataTab);
  275. this.selectTab(tabName);
  276. this.activateContent(tabName);
  277. Url.setHash(tabName).updateLocation();
  278. }
  279. onClassClick(e) {
  280. let element = CDElement.get(e.target);
  281. while(!element.hasClass(ClassPage.StyleClasses.ClassItem))
  282. element = element.getParent();
  283. Url.goTo(`/class/${element.getAttribute(ClassPage.Attributes.DataClassName)}`);
  284. }
  285. onModeButtonClick(e) {
  286. const button = CDElement.get(e.target);
  287. const mode = button.getAttribute(ClassPage.Attributes.DataDisplayMode);
  288. this.switchMode(mode);
  289. button.addClass(ClassPage.StyleClasses.Selected);
  290. (mode === ClassPage.Mode.Tabs ? this.listModeButton : this.tabsModeButton).removeClass(ClassPage.StyleClasses.Selected);
  291. }
  292. };
  293. window_.on(DOM.Events.Load, (e) => {
  294. window.page = new ClassPage().start();
  295. });
  296. window_.on(DOM.Events.KeyDown, (e) => {
  297. if(window.page && e.key === 'Control')
  298. window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.CtrlPressed);
  299. });
  300. window_.on(DOM.Events.KeyUp, (e) => {
  301. if(window.page && e.key === 'Control')
  302. window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.CtrlPressed);
  303. });