class ClassPage { static ModeCookieName = 'doczilla-js-docs-class-page-mode'; static Mode = { Tabs: 'mode-tabs', List: 'mode-list' }; static StyleClasses = { Selected: 'selected', Active: 'active', CtrlPressed: 'ctrl-pressed', ParentsBranch: 'parents-branch', ClassItem: 'class-item', ClassName: 'class-name', ClassIcon: 'class-icon', Filler: 'filler', FullSourcePrompt: 'full-source-prompt', FullSourcePromptText: 'full-source-prompt-text', FullSourcePromptButton: 'full-source-prompt-button', Spacing: 'spacing', CmLink: 'cm-link', Hidden: 'hidden' }; static Attributes = { DataTab: 'data-tab', DataDisplayMode: 'data-display-mode', DataClassName: 'data-class-name', OnClick: 'onclick' }; static Messages = { NoMixins: 'This class has no mixins.', NoChildren: 'This class has no child classes.', NoMixedIn: 'This class is not mixed in any classes.', NoParents: 'This is a base class, which has no parent classes.', ShowFullSourceText: 'There were found another entities in the source file of this class. Would you like to see full source file?', HideFullSourceText: 'Full source file shown. Would you like to hide all entities except the target class?', PromptButtonText: 'OK', CmLinkTipPrefix: 'Ctrl+Click to go to class' }; start() { const tabElements = this.tabElements = { Editor: DOM.get('.tab.editor'), Methods: DOM.get('.tab.methods'), Parents: DOM.get('.tab.parents'), Properties: DOM.get('.tab.properties'), Mixins: DOM.get('.tab.mixins'), Children: DOM.get('.tab.children'), MixedIn: DOM.get('.tab.mixedin') }; const contentElements = this.contentElements = { Editor: DOM.get('.content-tab#editor'), Methods: DOM.get('.content-tab#methods'), Parents: DOM.get('.content-tab#parents'), Properties: DOM.get('.content-tab#properties'), Mixins: DOM.get('.content-tab#mixins'), Children: DOM.get('.content-tab#children'), MixedIn: DOM.get('.content-tab#mixedin') }; const rightContainer = this.rightContainer = DOM.get('.right'); const selectedTab = this.selectedTab = tabElements[Url.getHash()] || tabElements.Editor; const activeContent = this.activeContent = contentElements[Url.getHash()] || contentElements.Editor; const modeCookieValue = DOM.getCookieProperty(App.CookieName, ClassPage.ModeCookieName); if(!modeCookieValue) DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, ClassPage.Mode.Tabs); const mode = this.mode = modeCookieValue || ClassPage.Mode.Tabs; const tabsModeButton = this.tabsModeButton = DOM.get('.display-mode-button.mode-tabs'); const listModeButton = this.listModeButton = DOM.get('.display-mode-button.mode-list'); (mode === ClassPage.Mode.Tabs ? tabsModeButton : listModeButton).addClass(ClassPage.StyleClasses.Selected); rightContainer.addClass(mode); this.renderContent(); this.selectTab(selectedTab); this.activateContent(activeContent); this.codeMirrorEditor.eachLine((lineHandle) => { this.markExtend(lineHandle); this.markMixins(lineHandle); }); this.registerEventListeners(); return this; } switchMode(mode) { this.rightContainer.removeClass(this.mode); this.mode = mode; DOM.setCookieProperty(App.CookieName, ClassPage.ModeCookieName, mode); this.rightContainer.addClass(mode); this.codeMirrorEditor.refresh(); } selectTab(tab) { tab = typeof tab === 'string' ? this.tabElements[tab] : tab; const selectedTab = this.selectedTab; let filler = selectedTab.getFirstChild('.filler'); if(!filler) filler = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Filler }); selectedTab.removeClass(ClassPage.StyleClasses.Selected); this.selectedTab = tab.addClass(ClassPage.StyleClasses.Selected); this.selectedTab.append(filler); } activateContent(content) { content = typeof content === 'string' ? this.contentElements[content] : content; this.activeContent.removeClass(ClassPage.StyleClasses.Active); this.activeContent = content.addClass(ClassPage.StyleClasses.Active); if(content === this.contentElements.Editor) this.codeMirrorEditor.refresh(); } registerEventListeners() { this.registerTabsEventListeners(); this.registerModeButtonsEventListeners(); } registerTabsEventListeners() { const tabElements = this.tabElements; for(const tabName of Object.keys(tabElements)) { tabElements[tabName].on(DOM.Events.Click, this.onTabClick.bind(this)); } } registerModeButtonsEventListeners() { this.tabsModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this)); this.listModeButton.on(DOM.Events.Click, this.onModeButtonClick.bind(this)); } prepareSource() { const className = Class.name.replaceAll('.', '\\.'); const classRx = new RegExp(`Z8\\.define\\(\'${className}\',\\s*\\{(?:.|[\r\n])+?^\\}\\);?`, 'gm'); const classSource = this.classSource = ClassSource.match(classRx)[0]; this.sourceHasAnotherEntities = ClassSource.trim() !== classSource; return classSource; } renderContent() { this.renderEditor(); this.renderParents(); this.renderMixins(); this.renderChildren(); this.renderMixedIn(); } renderEditor() { this.codeMirrorEditor = CodeMirror(DOM.get('#editor').get(), { value: this.prepareSource(ClassSource), mode: 'javascript', theme: 'darcula', readOnly: true, lineNumbers: true, matchBrackets: true, scrollbarStyle: 'overlay' }); if(this.sourceHasAnotherEntities) this.renderFullSourcePrompt(); this.codeMirrorEditorElement = DOM.get('.CodeMirror'); } renderMixins() { const mixinsElement = this.contentElements.Mixins; if(Class.mixins.length == 0) { DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixins }, mixinsElement); return; } this.renderClassItems(Class.mixins, mixinsElement); } renderChildren() { const childrenElement = this.contentElements.Children; if(Class.children.length == 0) { DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoChildren }, childrenElement); return; } this.renderClassItems(Class.children, childrenElement); } renderMixedIn() { const mixedInElement = this.contentElements.MixedIn; if(Class.mixedIn.length == 0) { DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoMixedIn }, mixedInElement); return; } this.renderClassItems(Class.mixedIn, mixedInElement); } renderFullSourcePrompt() { const editorContent = this.contentElements.Editor; const prompt = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePrompt }, editorContent); const text = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptText, innerHTML: ClassPage.Messages.ShowFullSourceText }, prompt); const button = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.FullSourcePromptButton, innerHTML: ClassPage.Messages.PromptButtonText }, prompt); const onButtonClick = (e) => { text.setInnerHTML(this.switchFullSource() ? ClassPage.Messages.HideFullSourceText : ClassPage.Messages.ShowFullSourceText); }; button.on(DOM.Events.Click, onButtonClick.bind(this)); } switchFullSource() { const show = this.fullSourceCodeShown = !this.fullSourceCodeShown; this.codeMirrorEditor.setValue(show ? ClassSource : this.classSource); if(show) this.findAndScrollToTargetClass(); this.codeMirrorEditor.refresh(); return show; } findAndScrollToTargetClass() { const className = Class.name.replaceAll('.', '\\.'); const editor = this.codeMirrorEditor; const defineRx = new RegExp(`Z8\\.define\\(\'${className}\',`); editor.eachLine((lineHandle) => { const text = lineHandle.text; const match = text.match(defineRx); if(match) { editor.scrollIntoView({ line: lineHandle.lineNo(), ch: 0 }); return; } }); } renderParents() { const parentsContent = this.contentElements.Parents; const parentsContainer = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ParentsBranch }, parentsContent); if(Class.parentsBranch.length == 0) { DOM.create({ tag: DOM.Tags.Div, style: 'font-size: 24px;', innerHTML: ClassPage.Messages.NoParents }, parentsContent); return; } this.renderClassItems(Class.parentsBranch, parentsContainer, true); } renderClassItems(itemsList, container, withIndent) { let indent = 0; if(!withIndent) itemsList = itemsList.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); for(const cls of itemsList) { const icon = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassIcon }); const name = DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.ClassName, innerHTML: cls }); const cn = [icon, name]; if(indent > 0 && withIndent) cn.unshift(DOM.create({ tag: DOM.Tags.Div, cls: ClassPage.StyleClasses.Spacing, style: `width: ${10 * indent}px;` })); 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)); indent++; } } markExtend(lineHandle) { const editor = this.codeMirrorEditor; const text = lineHandle.text; const match = text.match(/extend:\s*['"]?([\w\.]+)['"]?/); if (match) { const className = match[1]; const from = { line: lineHandle.lineNo(), ch: match.index + 8 }; const to = { line: lineHandle.lineNo(), ch: match.index + match[0].length }; editor.markText(from, to, { className: ClassPage.StyleClasses.CmLink, title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`, attributes: { [ClassPage.Attributes.DataClassName]: className, [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);' } }); } } markMixins(lineHandle) { const editor = this.codeMirrorEditor; const text = lineHandle.text; const match = text.match(/mixins:\s*(\[.*?\]|\w+)/); if (match) { let mixins = match[1].replace(/\[|\]/g, "").split(/\s*,\s*/); const mixinsStr = match[1].replace(/\[|\]/g, ""); mixins = mixinsStr.split(/\s*,\s*/); const startIndex = match.index + match[0].indexOf(mixinsStr); for (var i = 0; i < mixins.length; i++) { const className = mixins[i].trim().replace(/^['"]|['"]$/g, ""); const classIndex = mixinsStr.indexOf(className); const from = { line: lineHandle.lineNo(), ch: startIndex + classIndex }; const to = { line: lineHandle.lineNo(), ch: startIndex + classIndex + className.length }; editor.markText(from, to, { className: ClassPage.StyleClasses.CmLink, title: `${ClassPage.Messages.CmLinkTipPrefix} ${className}`, attributes: { [ClassPage.Attributes.DataClassName]: className, [ClassPage.Attributes.OnClick]: 'window.page.onClassLinkClick(this);' } }); } } } onClassLinkClick(fragment) { if(this.codeMirrorEditorElement.hasClass(ClassPage.StyleClasses.CtrlPressed)) Url.goTo(`/class/${fragment.getAttribute(ClassPage.Attributes.DataClassName)}`); } onTabClick(e) { const element = CDElement.get(e.target); if(element.hasClass(ClassPage.StyleClasses.Selected)) return; const tabName = element.getAttribute(ClassPage.Attributes.DataTab); this.selectTab(tabName); this.activateContent(tabName); Url.setHash(tabName).updateLocation(); } onClassClick(e) { let element = CDElement.get(e.target); while(!element.hasClass(ClassPage.StyleClasses.ClassItem)) element = element.getParent(); Url.goTo(`/class/${element.getAttribute(ClassPage.Attributes.DataClassName)}`); } onModeButtonClick(e) { const button = CDElement.get(e.target); const mode = button.getAttribute(ClassPage.Attributes.DataDisplayMode); this.switchMode(mode); button.addClass(ClassPage.StyleClasses.Selected); (mode === ClassPage.Mode.Tabs ? this.listModeButton : this.tabsModeButton).removeClass(ClassPage.StyleClasses.Selected); } }; window_.on(DOM.Events.Load, (e) => { window.page = new ClassPage().start(); }); window_.on(DOM.Events.KeyDown, (e) => { if(window.page && e.key === 'Control') window.page.codeMirrorEditorElement.addClass(ClassPage.StyleClasses.CtrlPressed); }); window_.on(DOM.Events.KeyUp, (e) => { if(window.page && e.key === 'Control') window.page.codeMirrorEditorElement.removeClass(ClassPage.StyleClasses.CtrlPressed); });