UNPKG

oda-framework

Version:

It's an ES Progressive Framework based on the technology of Web Components and designed especially for creating custom UI/UX of any complexity for web and cross-platform PWA mobile applications.

1,382 lines (1,351 loc) 61.4 kB
const jupyter_path = import.meta.url.split('/').slice(0, -1).join('/'); const path = window.location.href.split('/').slice(0, -1).join('/'); window.run_context = Object.create(null); run_context.output_data = undefined; function log_recurse(obj) { if (obj === null) return 'null'; if (obj === undefined) return 'undefined'; switch (obj.constructor) { case undefined: { if (obj instanceof JupyterProxyElement) return obj; obj = Object.assign({}, obj); } case Function: case Object: { try { return JSON.stringify(obj, 0, 2); } catch (e) { return obj.toString(); } } case Array: { return '[' + obj.map(log_recurse) + ']'; } default: { if (obj instanceof HTMLElement) { } else if (typeof obj === 'object' || typeof obj === 'function') return obj.toString(); } } return obj; } window.log = (...e) => { e = e.map(log_recurse); run_context.output_data?.push(...e.map(v => { let mimeType = 'text/plain'; if (v instanceof HTMLElement) mimeType = 'html/text'; else if (v instanceof JupyterProxyElement) { return { 'jupyter/iframe': v }; } else v = v.toString(); return { [mimeType]: v }; })) } const console_warn = console.warn; window.warning = window.warn = console.warn = (...e) => { console_warn.call(window, ...e); run_context.output_data?.push({ 'html/text': '<b>warning</b>:\n'+[...e].join('\n') }); } const console_error = console.error; window.err = window.error = console.error = (...e) => { console_error.call(window, ...e); run_context.output_data?.push({ 'html/text': '<b>error:</b>\n'+ [...e].join('\n') }); } window.run_context = run_context; import { getLoader } from '../../components/tools/loader/loader.js'; // https://medium.com/@aszepeshazi/printing-selected-elements-on-a-web-page-from-javascript-a878ac873828 // https://github.com/szepeshazi/print-elements import { PrintElements } from './print_elements.js'; ODA({ is: 'oda-jupyter', imports: '@oda/button, @oda/markdown', template: ` <style> :host { @apply --vertical; @apply --flex; outline: none !important; overflow-y: auto !important; overflow-x: hidden !important; /*padding: 12px 6px 30px 6px;*/ padding-top: 14px; opacity: 0; transition: opacity 1s; /*background-color: var(--content-background);*/ scroll-behavior: smooth; position: relative; @apply --light; } @media print { .pe-preserve-print::-webkit-scrollbar { width: 0px; height: 0px; } } </style> <oda-jupyter-divider ~style="{zIndex: cells.length + 1}"></oda-jupyter-divider> <oda-jupyter-cell content @tap="cellSelect($for.item)" ~for="cells" :cell="$for.item" ~show="!$for.item.hidden"></oda-jupyter-cell> <div style="min-height: 90%"></div> `, command_replace(){ const el = this.$$('oda-jupyter-cell').find(el => el.cell.id === this.selectedCell.id); el?.control?.editor?.editor?.execCommand?.('replace') }, use_native_menu: true, output_data: [], cellSelect(item){ this['selectedCell'] = item; // this.$render(); }, tabindex:{ $def: 0, $attr: true }, savedIndex:{ $def: 0, $save: true }, get $saveKey(){ return this.notebook.url }, $keyBindings:{ "ctrl+home"(e){ this.selectedCell = this.cells[0]; }, enter(e){ this.editMode = true; }, arrowup(e){ e.preventDefault() if (!this.editMode && this.selectedCell.index > 0) this.selectedCell = this.cells[this.selectedCell.index - 1] }, arrowdown(e){ e.preventDefault(); if (!this.editMode && this.cells.length - 1 > this.selectedCell.index) this.selectedCell = this.cells[this.selectedCell.index + 1] }, async "ctrl+p"(e){ e.stopPropagation(); e.preventDefault(); this.printValue(); } }, $public: { $pdp: true, iconSize: 24, readOnly: false, file_path: String, get url() { if (this.file_path?.startsWith('http')) return this.file_path; if(this.file_path) return path + '/' + this.file_path; return ''; }, levelStep: { $def: 8, $save: true } }, $listeners:{ scroll(e){ this.jupyter_scroll_top = this.scrollTop; this.isScroll = true; this.debounce('isScroll', () => { this.isScroll = false; }, 300) }, resize(e){ this.jupyter_height = this.offsetHeight; } }, $pdp: { jupyter_scroll_top: 0, get jupyter_height(){ return this.offsetHeight; }, showLoader: false, get jupyter() { return this; }, get notebook() { this.style.visibility = 'hidden'; this.style.opacity = 0; const nb = new JupyterNotebook(this.url); nb.listen('ready', async (e) => { await this.$render(); this.style.scrollBehavior = 'auto'; this.scrollTop = this.scrollHeight; this.async(async () => { const auto_run = this.$$('oda-jupyter-cell').filter(i=>i.cell.autoRun).last; if(auto_run) await auto_run.run(true); this.scrollTop = 0; await this.$render(); if (!this.selectedCell && this.cells?.[this.savedIndex]) { this.selectedCell = this.cells[this.savedIndex]; await this.scrollToCell(this.selectedCell, 0, 1 ); } this.style.visibility = 'visible'; this.style.opacity = 1; this.style.scrollBehavior = 'smooth'; }, 1000); }) nb.listen('changed', async (e) => { if(this.selectedCell) { const selectedFromCells = this.cells.find(cell => cell.id === this.selectedCell.id); if(selectedFromCells && this.selectedCell !== selectedFromCells) this.selectedCell = selectedFromCells; } await this.$render(); if (e.detail.value) { const added = this.$$('oda-jupyter-cell').find(cell => cell.cell.id === this.selectedCell?.id); added.focus(); } this.fire('changed'); }) if (!this.url) { this.style.visibility = 'visible'; this.style.opacity = 1; } return nb; }, editors: { code: { label: 'Code', editor: 'oda-jupyter-code-editor', type: 'code' }, text: { label: 'Text', editor: 'oda-markdown', type: 'text' }, sheet: { label: 'Sheet', editor: 'oda-jupyter-sheet-editor', type: 'sheet' } }, selectedCell: { $def: null, set(n, o) { if (n){ this.editMode = false; this.savedIndex = n.index; } else if (o){ this.selectedCell = o } } }, get cells() { return this.notebook?.cells; }, editMode: { $def: false, set(n){ if (n && this.readOnly) this.editMode = false; } } }, async scrollToCell(cell = this.selectedCell, row = 0, delta = 0, toLastRange = false) { if (!cell) return; const cellElements = this.jupyter.$$('oda-jupyter-cell'); const cellElement = cellElements.find(el => el.cell.id === cell.id); if (!cellElement) return; const screenTop = this.jupyter.scrollTop, screenBottom = screenTop + this.jupyter.offsetHeight; if (cell.type === 'code' && !cell.hideCode && row >= 0) { if (!cellElement?.control?.editor) return; if (toLastRange && cell.lastRange) { row = cell.lastRange.start.row; this.async(() => { cellElement.control.ace.focus(); }, 100) } const lineHeight = cellElement.control.editor.lineHeight, rowTop = cellElement.offsetTop + lineHeight * row, isVisible = rowTop >= screenTop && rowTop <= screenBottom - lineHeight * 2; if (isVisible && !delta) return; if (rowTop < screenTop || delta || toLastRange) delta = lineHeight * (row) - delta; else delta = lineHeight * (row + 2) - this.jupyter.offsetHeight - delta; this.jupyter.scrollTop = cellElement.offsetTop + delta; await new Promise(resolve => this.async(resolve, 100)); } else { if (delta) { this.jupyter.scrollTop = cellElement.offsetTop + delta; await new Promise(resolve => this.async(resolve, 100)); } else if (cellElement.offsetTop >= screenBottom - cellElement.offsetHeight) { this.jupyter.scrollTop += cellElement.offsetHeight; await new Promise(resolve => this.async(resolve, 100)); } } }, async attached() { await getLoader(); }, async printValue() { const scrollTop = this.scrollTop; this.style.scrollBehavior = 'auto'; this.scrollTop = this.scrollHeight; this.style.scrollBehavior = 'smooth'; this.scrollTop = 0; while (this.scrollTop>0){ await new Promise(resolve => this.async(resolve, 100)); } this.ownerDocument.body.classList.add("pe-preserve-ancestor"); PrintElements.print([this]); // this.ownerDocument.defaultView.print(); this.ownerDocument.body.classList.remove("pe-preserve-ancestor"); this.scrollTop = scrollTop; // this.scrollToCell(this.selectedCell); }, setFullscreen() { const element = this; if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } }, createElement(...args){ return ODA.createElement(...args); } }) ODA ({ is: 'oda-jupyter-cell-out', template: ` <style> [text-mode]{ padding: 4px; user-select: text; overflow-x: auto; } label { width: fit-content; } label:hover{ text-decoration: underline; cursor: pointer !important; } label[selected]{ @apply --content; text-decoration: underline; } iframe { border: none; width: 100%; height: 40px; } </style> <div :src="image" ~is="out_tag" :srcdoc vertical ~html="outHtml" ~style="{overflowWrap: (textWrap ? 'break-word': ''), whiteSpace: (textWrap ? 'break-spaces': 'pre')}" :text-mode="typeof outHtml === 'string'" :warning @load="iframe_loaded"></div> <div ~if="curRowsLength<maxRowsLength && !showAll" class="horizontal left header flex" style="font-size: small; align-items: center;"> <span style="padding: 9px;">Rows: {{curRowsLength.toLocaleString()}} of {{maxRowsLength.toLocaleString()}}</span> <oda-button ~if="!showAll" :icon-size class="dark border" style="margin: 4px; border-radius: 2px;" @tap="setStep($event, 1)">Show next {{(max*2).toLocaleString()}}</oda-button> <oda-button ~if="!showAll" :icon-size class="dark border" style="margin: 4px; border-radius: 2px;" @tap="showAll=true">Show all</oda-button> </div> `, textWrap: true, row: undefined, showAll: false, max: 50, step: 0, setStep(e, sign) { e.preventDefault(); e.stopPropagation(); this.step += sign; }, iframe_loaded(e){ const iframe = e.target; iframe.style.height = '40px'; this.row.item.elem = iframe.contentDocument.body.firstChild; console.log(this.row.item.elem); const resizeObserver = new ResizeObserver((e) => { let h = iframe.contentDocument.body.scrollHeight; iframe.style.height = h + 'px'; // if (h === iframe.contentDocument.body.scrollHeight) { // resizeObserver.unobserve(iframe.contentDocument.body); // } }) resizeObserver.observe(iframe.contentDocument.body); this.async(() => { this.$('#out-frame')?.scrollIntoView({ block: "end" }); }, 500) }, get srcdoc(){ let tag_name = this.row.item.tag_name, src = ''; try { src = JSON.stringify(ODA.telemetry.prototypes[tag_name] || {}); } catch (err) { console.log(err); } src = ` <${tag_name}></${tag_name}> <script type="module"> import '${jupyter_path.replace('tools/jupyter', 'oda.js')}'; ODA( ${src} ) <\/script> ` return src; }, get image(){ if (this.row?.key === 'image/png') return 'data:image/png;base64,' + this.row.item; }, get out_tag() { switch (this.row?.key){ case 'image/png': return 'img'; case 'jupyter/iframe': return 'iframe'; } return 'div'; }, get maxRowsLength(){ return this.split_out.length; }, get curRowsLength(){ return this.max * 2 * (this.step + 1); }, get split_out(){ const limit = 10000 return this.row?.item?.split?.('\n').map(v=>{ if(v.length>limit) return v.substring(0, limit); return v }) || []; }, get outHtml() { if (this.row?.item instanceof HTMLElement) return this.row.item; if (this.showAll) return this.row?.item || '' let array = this.split_out.slice(0, this.curRowsLength); return array.join('\n'); }, get warning() { return this.cell?.status === 'warning'; }, get error() { return this.cell?.status === 'error'; } }) ODA({ is: 'oda-jupyter-cell', imports: '@oda/menu', template: ` <style> :host{ padding-top: 2px; /*padding-left: 2px;*/ @apply --vertical; @apply --no-flex; position: relative; padding-right: 2px; margin-top: 4px; min-height: 24px; } .sticky{ cursor: pointer; position: sticky; top: 0px; } oda-icon{ cursor: pointer; max-height: 64px; } oda-button:hover{ border-radius: 50%; @apply --success-invert; } :host([raised]) .left-panel { @apply --header; } :host(:hover) { outline: 1px dashed gray; } :host(:hover) oda-jupyter-toolbar, :host(:hover) oda-jupyter-outputs-toolbar{ display: flex !important; } @media print { .pe-preserve-print { width: 100%!important; } } </style> <div class="horizontal"> <div class="pe-no-print left-panel vertical" :error-invert="status === 'error'" content style="z-index: 2;"> <div class="sticky" style="min-width: 40px; max-width: 40px; margin: -2px; margin-top: 2px; min-height: 50px; font-size: xx-small; text-align: center; white-space: break-spaces;" > <oda-button ~if="cell.type === 'code'" :icon-size :icon @tap="run()" :error="cell?.autoRun" :success="!cell?.time" style="margin: 4px; border-radius: 50%;"></oda-button> <div>{{time}}</div> <div>{{status}}</div> <oda-icon id="go-lastrange" ~if="!cell?.hideCode && isLastRange" :icon-size icon="box:s-edit-alt" @tap="scrollToLastRange" style="margin: 8px;" title="scroll to marked"></oda-icon> <oda-icon id="go-breakpoint" no-flex ~if="showGoBreakpoint" :icon-size icon="icons:label-outline" @tap="goToBreakPoint" style="margin: 8px;" title="debugger"></oda-icon> </div> </div> <div class="pe-preserve-print vertical no-flex" style="width: calc(100% - 34px); position: relative;"> <div id="main" class="vertical" > <oda-jupyter-toolbar :icon-size="iconSize * .7" :cell :control ~show="selected"></oda-jupyter-toolbar> <div class="horizontal" > <oda-icon ~if="cell.type!=='code' && cell.allowExpand" :icon="expanderIcon" @dblclick.stop @tap.stop="this.cell.collapsed = !this.cell.collapsed"></oda-icon> <div flex id="control" ~is="editor" :cell ::edit-mode ::value :read-only show-preview :_value :show-border="editMode"></div> </div> <div info ~if="cell.collapsed" class="horizontal" @tap="cell.collapsed = false"> <oda-icon style="margin: 4px;" :icon="childIcon"></oda-icon> <div style="margin: 8px;">Hidden {{cell.childrenCount}} cells</div> </div> </div> <div id="outputs" ~if="outputs?.length" class="info border" flex style="z-index: 1;"> <oda-jupyter-outputs-toolbar :icon-size="iconSize * .7" :cell ~show="selected"></oda-jupyter-outputs-toolbar> <div class="vertical flex" style="overflow: hidden;"> <div flex vertical ~if="!cell?.hideOutput" style="overflow: hidden;"> <div ~for="outputs" style="font-family: monospace;" > <oda-jupyter-cell-out ~for="$for.item.data" :row="$$for" :max="control?.maxRow"></oda-jupyter-cell-out> </div> </div> </div> <div ~if="cell?.hideOutput" class="horizontal left header" style="padding: 0 4px; font-size: small;"> <oda-button :icon-size class="dark header no-flex" style="margin: 4px; border-radius: 2px; cursor: pointer;" @tap="showOutput">Show hidden outputs data</oda-button> </div> </div> <div class="pe-no-print horizontal left header flex" ~if="!cell?.hideOutput && showOutInfo" style="padding: 0 4px; font-size: small; align-items: center; font-family: monospace;"> <span style="padding: 9px;">{{outInfo}}</span> <oda-button ~if="!showAllOutputsRow" :icon-size class="dark" style="margin: 4px; border-radius: 2px; cursor: pointer;" @tap="setOutputsStep($event, 1)">Show next {{maxOutputsRow.toLocaleString()}}</oda-button> <oda-button ~if="!showAllOutputsRow" :icon-size class="dark" style="margin: 4px; border-radius: 2px; cursor: pointer;" @tap="setOutputsStep($event, 0)">Show all</oda-button> </div> </div> </div> <oda-jupyter-divider></oda-jupyter-divider> `, get showGoBreakpoint() { return !this.cell?.hideCode && this.cell?.breakpoints?.trim(); }, get outputs(){ this.control = undefined; return this.cell?.controls?.slice(0, this.maxOutputsRow * (this.outputsStep + 1)) || []; }, get _value() { return `<b style="margin: 4px; cursor: pointer; align-self: center;"><u>Empty ${this.cell?.type}</u></b> <gray>(click for edit...)</gray>` }, get maxOutputsRow() { return this.control?.maxRow; }, get outInfo() { return `Blocks: ${this.showAllOutputsRow ? this.cell.outputs.length.toLocaleString() : Math.round(this.maxOutputsRow * (this.outputsStep + 1)).toLocaleString()} of ${this.cell?.outputs?.length.toLocaleString()}`; }, get showOutInfo() { return this.cell?.outputs?.length > this.maxOutputsRow; }, console(i, type) { i = Array.isArray(i) ? i.join('\n') : i; return i.startsWith(type); }, focus() { this.async(() => { this.$('#control').focus(); }, 300) }, get childIcon() { return this.cell.childCodes.length ? 'av:play-circle-outline' : 'bootstrap:text-left'; }, get editor() { return this.editors[this.cell.type]?.editor ?? this.editors.text.editor; }, value: { get() { return this.cell.src; }, set(n) { this.cell.src = n } }, $listeners:{ dblclick(e){ if (!this.readOnly) { this.editMode = true; this.$render(); } }, click(e){ if (!this.readOnly && !this.value) { this.editMode = true; this.$render(); } }, resize(e) { this.control_height = undefined; this.cell_height = undefined; } }, get time(){ return this.cell?.time || ''; }, get status(){ return this.cell?.status || ''; }, async run(autorun) { return new Promise(resolve => { const task = ODA.addTask(); this.outputsStep = 0; this.showAllOutputsRow = false; const out = this.$$('oda-jupyter-cell-out'); if (out?.length) { out.map(i => { i.step = 0; i.showAll = false; }) } this.async(async () => { try { for (let code of this.notebook.codes){ if (code === this.cell) break; if (code.hideCode && !autorun) continue; if (code.time) continue; await new Promise(async (resolve)=>{ if (autorun !== true) this.checkBreakpoints(code); await code.run(this); this.async(resolve) }) await this.$render(); } if (autorun !== true) this.checkBreakpoints(this.cell); await this.cell.run(this); } catch (error) { } finally { ODA.removeTask(task); if(autorun !== true) this.notebook?.change(); resolve(); if(autorun !== true){ this.cell?.next?.clearTimes(); this.scrollToRunOutputs(); } this.async(() => { this.$render(); }) } }, 50) }) }, checkBreakpoints(cell) { cell.srcWithBreakpoints = ''; if (cell.hideCode || !cell.breakpoints) return; const control = this.jupyter.$$('oda-jupyter-cell').find(i => i.cell.id == cell.id)?.control, session = control?.session; if (!session) return; let src = ''; const breakpoints = cell.breakpoints.split(' '), obj = {}; breakpoints.map(i => obj[i - 1] = true); session.doc.getAllLines().map((i, idx) => { if (obj[idx]) { if (i?.trim().startsWith('>')) { i= i.trim(); let count = 0; while(i[0] === '>') { count += 1; i = i.slice(1); } } src += 'debugger;\n' + i + '\n'; } else { src += i + '\n'; } }) cell.srcWithBreakpoints = src; }, goToBreakPoint(e) { let breakpoints = this.cell.breakpoints.split(' '); breakpoints = breakpoints.filter(i => i).sort((a, b) => a - b); let row = this._currentBreakPoints, isChange = false; if (!this._currentBreakPoints || breakpoints.length === 1) { row = +breakpoints[0]; } else { for (let i = 0; i < breakpoints.length; i++) { const p = breakpoints[i]; if (p > row) { row = +p; isChange = true; break; } } if (!isChange) { row = +breakpoints[0]; } } this._currentBreakPoints = row; row -= 1; const delta = this.$('#go-breakpoint').offsetTop + 12; this.jupyter.scrollToCell(this.cell, row, delta); }, get isLastRange() { return this.cell?.lastRange; }, scrollToLastRange() { if (this.cell?.lastRange) { const delta = this.$('#go-lastrange').offsetTop + 12; this.jupyter.scrollToCell(this.cell, this.cell.lastRange.start.row, delta, true); } }, scrollToRunOutputs() { this.async(() => { if (this.control_bottom < (this.jupyter_height + this.jupyter_scroll_top) /*&& this.control_offsetBottom > this.jupyter_scroll_top*/) return; if (this.cell?.hideOutput) return; if (this.output_height>this.jupyter_height) this.jupyter.scrollTop = this.control_bottom - 64; else this.$('#outputs')?.scrollIntoView({block: "end"}); }) }, get output_height() { return this.cell_height - this.control_height; }, get control_height() { return this.control.offsetHeight; }, get cell_height() { return this.offsetHeight; }, $pdp: { get control_bottom(){ return this.offsetTop + this.control_height; }, get icon(){ return this.cell?.isRun? 'spinners:8-dots-rotate': 'av:play-circle-outline'; }, get isReadyRun(){ return this.selected || this.cell?.isRun; }, editMode: { $def: false, get() { return !this.readOnly && this.jupyter.editMode && this.selected; }, set(n) { this.jupyter.editMode = n; } }, cell: null, selected:{ $def: false, $attr: 'raised', get() { return !this.readOnly && (this.selectedCell === this.cell/* || this.selectedCell?.id === this.cell?.id*/); } }, get control() { return this.$('#control') || undefined; }, showAllOutputsRow: false, outputsStep: 0 }, setOutputsStep(e, sign) { e.preventDefault(); e.stopPropagation(); this.outputsStep += sign; if (this.outputsStep > this.cell?.outputs?.length / this.maxOutputsRow - 1 || sign === 0) { this.outputsStep = this.cell.outputs.length / this.maxOutputsRow - 1; this.showAllOutputsRow = true; } }, get expanderIcon() { return this.cell.collapsed ? 'icons:chevron-right' : 'icons:expand-more'; }, showOutput() { this.cell.hideOutput = false; this.jupyter.$render(); this.notebook.change(); }, create(tag_name, props = {}){ return new JupyterProxyElement(tag_name, props = {}); } }) class JupyterProxyElement{ #props = {}; #tag_name = ''; #elem = {}; constructor(tag_name, props = {}) { this.#props = props; this.#tag_name = tag_name; return new Proxy(this, { get(target, p) { if (p !== 'constructor') { return target.elem?.[p] || target[p]; } }, set(target, p, value) { target[p] = value; if (target.elem) target.elem[p] = value; return true; } }) } get tag_name() { return this.#tag_name; } get elem() { return this.#elem; } set elem(n) { this.#elem = n; for (p in this.#props) this.elem[p] = p; } } ODA({ is: 'oda-jupyter-divider', template: ` <style> :host { @apply --vertical; height: 3px; justify-content: center; opacity: {{!visible?0:1}}; margin-top: {{last?'12px':'0px'}}; position: relative; z-index: 10; } :host(:hover) { opacity: 1 !important; } oda-button { font-size: 14px; margin: -4px 4px 0 4px; @apply --content; @apply --border; padding: 0px 4px 0px 0px; border-radius: 4px; opacity: 1; } </style> <div class="pe-no-print horizontal center" style="z-index: 2"> <oda-button ~if="!readOnly" :icon-size icon="icons:add" ~for="editors" @tap.stop="add($for.key)">{{$for.key}}</oda-button> <oda-button ~if="showInsertBtn()" :icon-size icon="icons:add" @tap.stop="insert" style="color: red; fill: red">Insert cell - {{showInsertBtn()}} </oda-button> </div> `, get last() { return this.cell?.isLast; }, get visible(){ if (!this.cells?.length) return true; if(this.cell?.isLast) return true; return false }, add(key) { this.jupyter.isMoveCell = true; this.selectedCell = this.notebook.add(this.cell, key); this.async(() => { if (!this.selectedCell.next) { this.scrollToCell(this.selectedCell, -1); } this.async(() => this.jupyter.isMoveCell = false, 300); }, 300) }, showInsertBtn() { return !this.readOnly && JSON.parse(top._jupyterCellData || '[]')?.length; }, insert() { this.jupyter.isMoveCell = true; const cells = JSON.parse(top._jupyterCellData || '[]'); this.selectedCell = this.cell; let lastCell; cells.map(i => { lastCell = this.notebook.add(lastCell || this.cell, '', i); lastCell.id = lastCell.metadata.id = getID(); }) this.selectedCell ||= lastCell; top._jupyterCellData = undefined; this.async(() => { this.jupyter.isMoveCell = false; }, 300) } }) ODA({ is: 'oda-jupyter-toolbar', imports: '@tools/containers, @tools/property-grid', template: ` <style> :host{ position: sticky; top: 20px; z-index: 11; } .top { @apply --horizontal; @apply --no-flex; @apply --content; @apply --raised; position: absolute; right: 8px; padding: 1px; border-radius: 4px; margin-top: -20px; } oda-button{ border-radius: 4px; } </style> <div class="pe-no-print top" ~if="!readOnly" @down="tap"> <oda-button :disabled="!cell.prev" :icon-size icon="icons:arrow-back:90" @tap.stop="move(-1)"></oda-button> <oda-button :disabled="!cell.next" :icon-size icon="icons:arrow-back:270" @tap.stop="move(1)"></oda-button> <oda-button ~show="cell?.type === 'code' || cell?.type === 'sheet'" :icon-size icon="icons:settings" @tap.stop="showSettings"></oda-button> <oda-button :icon-size icon="icons:delete" @tap.stop="deleteCell"></oda-button> <oda-button :icon-size icon="icons:content-copy" @tap.stop="copyCell" ~style="{fill: isCopiedCell ? 'red' : ''}"></oda-button> <oda-button ~if="cell.type!=='code'" allow-toggle ::toggled="editMode" :icon-size :icon="editMode?'icons:close':'editor:mode-edit'"></oda-button> <oda-button ~if="cell?.type === 'code'" :icon-size icon="bootstrap:eye-slash" title="Hide/Show code" allow-toggle ::toggled="hideCode"></oda-button> </div> `, get hideCode(){ return this.cell?.hideCode; }, set hideCode(n){ let top = this.jupyter.scrollTop; if (n){ if (top > this.domHost.offsetTop){ this.async(()=>{ this.jupyter.scrollToCell(this.selectedCell); }) //top -= this.domHost.$('#main').offsetHeight - (top - this.domHost.$('#main').offsetTop); } } else{ } // this.cell.hideCode = this.cell.hideOutput = n; this.control.hideCode = this.cell.hideOutput = n; }, move(direction){ let top = this.jupyter.scrollTop; if(direction<0){ top -= this.domHost.previousElementSibling.offsetHeight } else if(direction>0){ top += this.domHost.nextElementSibling.offsetHeight } this.cell.move(direction); this.jupyter.scrollTop = top; this.setIsMoveCell(); }, cell: null, iconSize: 16, deleteCell() { if (!window.confirm(`Do you really want delete current cell?`)) return; this.cell.delete(); this.setIsMoveCell(); }, setIsMoveCell() { this.jupyter.isMoveCell = true; this.async(() => this.jupyter.isMoveCell = false, 500); }, control: null, showSettings(e) { ODA.showDropdown('oda-property-grid', { inspectedObject: this.control, filterByFlags: '' }, { minWidth: '480px', parent: e.target, anchor: 'top-right', align: 'left', title: 'Settings', hideCancelButton: true }) }, get isCopiedCell() { let cells = JSON.parse(top._jupyterCellData || '[]'); return cells.find(i => i.metadata.id === this.cell.metadata.id); }, copyCell() { let cell, cells = JSON.parse(top._jupyterCellData || '[]'); if (cells?.length) { cell = cells.find(i => i.metadata.id === this.cell.metadata.id); if (cell) { cells = cells.filter(i => i.metadata.id !== this.cell.metadata.id); if (cells.length === 0) { top._jupyterCellData = this.isCopiedCell = undefined; } else { top._jupyterCellData = JSON.stringify(cells); this.isCopiedCell = undefined; } } } if (!cell) { cells.push(this.cell.data); top._jupyterCellData = JSON.stringify(cells); } this.jupyter.$render(); }, tap(e) { this.selectedCell = this.cell; } }) ODA({ is: 'oda-jupyter-outputs-toolbar', template: ` <style> :host{ position: sticky; top: 20px; z-index: 9; display: block; } .top { @apply --horizontal; @apply --no-flex; @apply --content; @apply --raised; right: 8px; position: absolute; padding: 1px; border-radius: 4px; margin-top: 4px; } oda-button{ border-radius: 4px; } </style> <div class="pe-no-print top info border" ~if="cell?.outputs?.length || cell?.controls?.length"> <oda-button :icon-size icon="bootstrap:eye-slash" title="Hide/Show" allow-toggle ::toggled="toggleOutput"></oda-button> <oda-button :icon-size icon="icons:clear" @tap="clearOutputs" title="Clear outputs"></oda-button> </div> `, cell: null, iconSize: 16, get toggleOutput(){ return this.cell.hideOutput; }, set toggleOutput(n){ this.cell.hideOutput = n; this.jupyter.$render(); this.notebook.change(); }, clearOutputs() { this.cell.hideOutput = false; this.cell.outputs = []; this.cell.controls = []; } }) ODA({ is: 'oda-jupyter-sheet-editor', imports: '@oda/jspreadsheet-editor', extends: 'oda-jspreadsheet-editor', template:` <style> :host { height: {{height}}px; } </style> `, $public: { height: { get(){ return this.cell?.readMetadata('height', 160); }, set(n){ this.cell?.writeMetadata('height', n); } } }, set editMode(v) { this.toolbar = this.tabs = v; }, $listeners: { 'sheet-tap'(e) { this.selectedCell = this.cell; } } }) const AsyncFunction = async function () {}.constructor; ODA({ is: 'oda-jupyter-code-editor', imports: '@oda/code-editor', template: ` <style> :host { @apply --vertical; @apply --flex; position: relative; } .sticky{ cursor: pointer; position: sticky; top: 0px; } oda-button:hover{ border-radius: 50%; @apply --active; } oda-code-editor { opacity: 1; filter: unset; } .err { cursor: pointer; color: red; opacity: 0; font-size: larger; font-weight: bold; } .err:hover { opacity: 1; } </style> <div class="horizontal" :border="!hideCode" style="min-height: 64px;"> <oda-code-editor :scroll-calculate="getScrollCalculate()" :wrap ~if="!hideCode" show-gutter :read-only @change-cursor="on_change_cursor" @change-breakpoints="on_change_breakpoints" @keypress="_keypress" :src="value" mode="javascript" font-size="12" class="flex" max-lines="Infinity" @change="editorValueChanged" @pointerdown="on_pointerdown" enable-breakpoints sticky-search></oda-code-editor> <div dimmed ~if="hideCode" class="horizontal left content flex" style="cursor: pointer; padding: 8px 4px;" @dblclick="hideCode=false"> <oda-icon icon="bootstrap:eye-slash" style="align-self: baseline; cursor: pointer;" @tap="hideCode = false"></oda-icon> <h1 flex vertical style="margin: 0px 16px; font-size: large; cursor: pointer; text-overflow: ellipsis;" ~html="cell.name +'... <u disabled style=\\\'font-size: x-small; right: 0px;\\\'>(Double click to show...)</u>'" ></h1> </div> </div> <div ~if="syntaxError" border vertical style="padding: 4px; z-index: 1;"> <div border style="padding: 4px; font-family: monospace; white-space: pre" border error style="white-space: pre-wrap" ~html="syntaxError"></div> </div> `, on_pointerdown(e) { this.ace?.focus(); }, on_change_cursor(e) { this.debounce('change_cursor', () => { if (!this.jupyter.isMoveCell && !this.jupyter.isScroll) { this.cell.lastRange = this.ace?.getSelectionRange(); // if (this.cell.lastRange.start.row === this.cell.lastRange.end.row && this.cell.lastRange.start.column === this.cell.lastRange.end.column) { this.jupyter.scrollToCell(this.cell, this.cell.lastRange.start.row); // } } }, 300) }, on_change_breakpoints(e){ this.cell.breakpoints = e.detail.value; }, get syntaxError(){ let error = this.editor?.editor?.session?.getAnnotations(); error = error?.filter((e, i)=>{ if(e.type !== 'error') return false; if (this.jupyter?.notebook?.data?.hiddenErrors?.some(s => s.startsWith(e.text.replace(/\s*from\s*line\s*\d*\s*/gm, '')))) return false; if(e.text.startsWith('Expected an identifier and instead saw \'>')) return false; if(e.text.startsWith('Unexpected early end of program.')) return false; if(e.text.startsWith('Missing ";" before statement')) return false; if(e.text.startsWith(`Expected an identifier and instead saw '='.`) && error[i+1]?.text.startsWith(`Unexpected '{a}'.`)) return false; if(e.text.startsWith(`Unexpected '{a}'.`) && error[i-1]?.text.startsWith(`Expected an identifier and instead saw '='.`)) return false; return true; }).map(err =>{ return `<span class="err" onclick="_hideError(this)"> x </span><span>${err.text}</span><u row="${err.row}" column="${err.column}" onclick="_findErrorPos(this)" style="cursor: pointer; color: -webkit-link">(${err.row+1}:${err.column})</u>` }).join('\n'); if(error) error = '<span bold style="padding: 2px; font-size: large; margin-bottom: 4px; white-space: pre-wrap;">SyntaxError:</span><br>'+error; return error; }, get editor(){ return this.$('oda-code-editor'); }, get ace(){ return this.editor?.editor; }, get session(){ return this.ace?.session; }, focus() { this.editor?.focus(); }, _keypress(e){ if (e.ctrlKey && e.keyCode === 10){ this.domHost.run(); } }, value: '', editorValueChanged(e) { this.syntaxError = undefined; this.value = e.detail.value; this.debounce('erros', () => { this.syntaxError = undefined; this.$render(); }, 700) }, $public:{ autoRun:{ $type: Boolean, get(){ return this.cell?.autoRun; }, set(n){ this.cell.autoRun = n; } }, wrap: { $def: false, get(){ return this.cell?.readMetadata('wrap', false) }, set(n){ this.cell?.writeMetadata('wrap', n) } }, hideCode: { $def: false, get(){ return this.cell?.hideCode }, set(n){ this.cell.hideCode = n; this.editor = undefined; this.setBreakpoints(); } }, maxRow:{ // $group: 'output', $pdp: true, $def: 50, get(){ return this.cell?.readMetadata('maxRow', 50) }, set(n){ this.cell?.writeMetadata('maxRow', n) } }, clearHiddenErros: { $def: '', get() { let l = this.jupyter?.notebook?.data?.hiddenErrors?.length; return l ? l + ' hidden errors' : ''; }, set(n) { let l = this.jupyter?.notebook?.data?.hiddenErrors?.length; if (l && l !== l + ' hidden errors') { this.jupyter.notebook.clearHiddenErrors(); } } } }, getScrollCalculate(){ return this.jupyter_height - 22 - 5; }, attached() { this.setBreakpoints(); this.cell.listen('change-breakpoints', () => this.setBreakpoints()); }, setBreakpoints() { this.async(() => { if (this.cell.breakpoints) { this.editor?.setBreakpoints(this.cell.breakpoints, true); this.$render(); } }, 700) } }) class JupyterNotebook extends ROCKS({ data: { cells: [], hiddenErrors: [] }, isChanged: false, get cells() { return this.data.cells.map(cell => new JupyterCell(cell, this)); }, get codes() { return this.cells.filter(i=>i.type === 'code'); }, get items() { return this.cells.filter(cell => cell.level === 0); }, async load(url) { try { this.data = await ODA.loadJSON(url); this.data.cells.forEach(c=>c.time = ''); this.url = url; } catch (err) { } finally { this.fire('ready'); } }, save(url) { //todo save this.isChanged = false; }, add(cell, cell_type = 'text', data) { try { data = data ? JSON.parse(data) : null; } catch (err) { } let id = getID(); if (data?.metadata) { data.metadata.id = id; } data ||= { cell_type, source: '', metadata: { id } } if (cell === undefined){ this.data.cells.splice(0, 0, data); } else{ const idx = cell.index + 1; this.data.cells.splice(idx, 0, data); } this.async(() => { this.change(true); }) this.cells = undefined; return this.cells.find(i => i.id === id) }, addHiddenError(err) { this.data.hiddenErrors ||= []; this.data.hiddenErrors.add(err); this.change(); }, clearHiddenErrors() { this.data.hiddenErrors = []; this.change(); }, change(add_new) { this.isChanged = true; this.fire('changed', add_new); }, name: 'NOTEBOOK', get label(){ return this.name; }, icon: 'fontawesome:s-book' }) { url = ''; constructor(url) { super(); if (url){ this.load(url); this.name = url.split('/').pop(); } } } class JupyterCell extends ROCKS({ data: null, notebook: null, isRun: false, type: { $def: 'text', $list: ['text', 'code'], get() { return this.data.cell_type; } }, get items() { return this.notebook.cells.filter(cell => cell.parent?.id === this.id); }, get parent() { let prev = this.prev; while(prev && prev.level !== this.level - 1) { prev = prev.prev; } return prev; }, get name() { const firstSource = this.src.trim().split('\n')[0]; let t = this.type; switch (this.type) { case 'text': case 'markdown': return firstSource.substring(this.h).trim() || (t + ' [empty]'); case 'code': return firstSource ?? ' [empty]'; } return '???' }, get time(){ return this.data.time; }, set time(n){ return this.data.time = n; }, get metadata() { return this.data.metadata; }, get id() { return this.metadata?.id || getID(); }, get controls() { return this.data?.controls || this.outputs; }, set controls(n) { this.outputs = n; Object.defineProperty(this.data, 'controls', {