oda-framework
Version:
1,362 lines (1,334 loc) • 66.9 kB
JavaScript
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: {
try {
return JSON.stringify(obj, 0, 2);
}
catch (e) {
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(...e.map(v => {
return { 'html/text': '<span style="font-size: large;" info><b>warning: </b>' + v.toString() +'</span>' };
}))
}
const console_error = console.error;
window.err = window.error = console.error = (...e) => {
console_error.call(window, ...e);
run_context.output_data?.push(...e.map(v => {
return { 'html/text': '<span style="font-size: large;" error><b>error: </b>' + v.toString() +'</span>' };
}))
}
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 './lib/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-top: 14px;
scroll-behavior: smooth;
position: relative;
@apply --light;
z-index: 1;
}
@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="focusedCell = $for.item" ~for="cells" :cell="$for.item" ~show="!$for.item.hidden" :focused="focusedCell === $for.item"></oda-jupyter-cell>
<div style="min-height: 90%"></div>
`,
command_replace(){
const el = this.getCell(this.focusedCell.id);
el?.control?.editor?.editor?.execCommand?.('replace')
},
connect(target){
if (this.connected_target === target)
return;
if (this.connected_target){
this.connected_target.unlisten('progress-changed');
this.unlisten('stop');
}
this.connected_target = target;
target.listen('progress-changed', async progress=>{
await this.setProgress(progress);
})
this.listen('stop', e=>{
target.stop = true;
setTimeout(()=>{
target.stop = false;
}, 100)
})
},
use_native_menu: true,
output_data: [],
tabindex:{
$def: 0,
$attr: true
},
savedIndex:{
$def: 0,
$save: true
},
get $saveKey(){
return this.notebook.url
},
$keyBindings:{
"ctrl+home"(e){
this.focusedCell = this.cells[0];
},
enter(e){
this.editMode = true;
},
arrowup(e){
e.preventDefault()
if (!this.editMode && this.focusedCell.index > 0)
this.focusedCell = this.cells[this.focusedCell.index - 1]
},
arrowdown(e){
e.preventDefault();
if (!this.editMode && this.cells.length - 1 > this.focusedCell.index)
this.focusedCell = this.cells[this.focusedCell.index + 1]
},
async "ctrl+p"(e){
e.stopPropagation();
e.preventDefault();
this.printValue();
}
},
set stop(n){
this.fire('stop', n)
},
async setProgress(percent){
return await new Promise(resolve=>{
this.progress = percent;
requestAnimationFrame(()=>{
resolve(this.progress)
})
})
},
$public: {
progress: 0,
$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
},
maxOutputRows: {
$def: 20,
$save: true
},
},
$listeners:{
scroll(e){
this.jupyter_scroll_top = this.scrollTop;
this.jupyter.debounce('blink', ()=>{
this.getCell(this.focusedCell?.id).blink = false;
}, 200)
},
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';
const nb = new JupyterNotebook(this.url);
nb.listen('ready', async (e) => {
await this.$$('oda-jupyter-cell').filter(i=>i.cell.autoRun).last?.auto_run();
await this.$render();
this.async(() => {
this.focusedCell = this.cells?.[this.savedIndex];
this.async(()=>{
this.style.visibility = 'visible';
}, 100)
}, 1000);
})
nb.listen('changed', (e) => {
// if(this.focusedCell) {
// const selectedFromCells = this.cells.find(cell => cell.id === this.focusedCell.id);
// if(selectedFromCells && this.focusedCell !== selectedFromCells)
// this.focusedCell = selectedFromCells;
// }
// await this.$render();
if (e.detail.value) {
this.getCell(this.focusedCell?.id)?.focus?.();
}
this.fire('changed');
})
return nb;
},
editors: {
code: { label: 'Code', editor: 'oda-jupyter-code-editor', type: 'code' },
text: { label: 'Text', editor: 'oda-markdown', type: 'text' },
},
focusedCell: {
$def: null,
set(n, o) {
if (n){
this.editMode = false;
this.savedIndex = n.index;
let el = this.getCell(n.id);
if (!el)
return;
el.blink = true;
el.scrollToCell(o && o.index < n.index);
if (el.cell?.lastRange && el.control) {
if (el.scrollCancel || this.isMoveCell)
return;
this.async(() => {
el.control.scrollToCursor(el.cell.lastRange.start.row, 10);
this.isMoveCell = 1;
el.control.focus();
}, 100)
}
}
else if (o){
this.focusedCell = o
}
}
},
get cells() {
return this.notebook?.cells;
},
editMode: {
$def: false,
set(n){
if (n && this.readOnly)
this.editMode = false;
}
}
},
set isMoveCell(v) {
if (this.isMoveCell === 0)
return;
// console.log(v)
this._isMoveCell?.clearTimeout?.();
this._isMoveCell = setTimeout(() => {
this.isMoveCell = 0;
}, 500)
},
getCell(id){
return this.$$('oda-jupyter-cell').find(i => i.cell.id === id);
},
async attached() {
await getLoader();
this.listen('cell-action-run-next', e => {
const cell = e.detail?.value;
if (cell) {
// console.log(cell.id)
cell.jupyter ||= this;
let next = cell.next;
next?.execute?.(cell);
}
})
},
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.$$('oda-jupyter-cell'));
// this.ownerDocument.defaultView.print();
this.ownerDocument.body.classList.remove("pe-preserve-ancestor");
this.scrollTop = scrollTop;
this.focusedCell.scrollToCell();
},
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" light ~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).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>
`,
$wake: true,
textWrap: true,
row: undefined,
showAll: false,
get max() {
return this.maxOutputRows;
},
step: 1,
setStep(e, sign) {
e.preventDefault();
e.stopPropagation();
this.step += sign;
},
iframe_loaded(e){
const iframe = e.target;
if (!iframe.contentDocument) return;
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(){
if (this.row?.key !== 'jupyter/iframe') return '';
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 * 1 * (this.step + 1);
},
split_out(out = this.row?.item || '') {
const limit = 10000
return out.split?.('\n').map(v => {
if(v.length>limit)
return v.substring(0, limit);
return v
}) || [];
},
replaceAngleBrackets(htmlString) {
if (typeof htmlString !== 'string') return;
const validHtmlTags = new Set(['img', 'input', 'button']);
const regex = /<([^>\s]+)([^>]*)>/g;
function hasClosingTag(tag, str) {
const closingTag = `</${tag}>`;
return str.includes(closingTag);
}
return htmlString.replace(regex, (match, tagName, attributes) => {
if (tagName.startsWith('/')) {
const actualTagName = tagName.slice(1);
if (validHtmlTags.has(actualTagName) || actualTagName.match(/^[a-zA-Z][a-zA-Z0-9-_]*$/)) {
return match;
}
}
if (validHtmlTags.has(tagName))
return match;
if (tagName.match(/^[a-zA-Z][a-zA-Z0-9-_]*$/) && hasClosingTag(tagName, htmlString))
return match;
return `<${tagName}${attributes}>`;
})
},
get outHtml() {
if (this.row?.item instanceof HTMLElement)
return this.row.item;
let out = this.row.item;
if (!out.startsWith?.("<label bold onclick='_findCodeEntry(this)'"))
out = this.replaceAngleBrackets(out);
if (this.showAll)
return out || ''
let array = this.split_out(out).slice(0, this.curRowsLength);
return array.join('\n');
},
get warning() {
return this.cell?.status === 'warning';
},
get error() {
return this.cell?.status === 'error';
},
attached(e) {
this.showAll = false;
}
})
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) {
@apply --shadow;
}
:host(:hover) oda-jupyter-toolbar, :host(:hover) oda-jupyter-outputs-toolbar{
display: flex !important;
}
#block {
transition: filter .2s ease-in-out;
}
[blink]{
filter: sepia(.75);
}
@media print {
.pe-preserve-print {
width: 100%!important;
}
}
.circular-progress-container {
margin: 4px 0px 4px 4px;
}
.hidden-progress {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.circular-progress {
--size: 32px;
--border-width: 3px;
--progress: 0;
width: var(--size);
height: var(--size);
border-radius: 50%;
background: conic-gradient(
#3498db calc(var(--progress) * 3.6deg),
#eee 0deg
);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.circular-progress::before {
content: '';
position: absolute;
width: calc(100% - var(--border-width) * 2);
height: calc(100% - var(--border-width) * 2);
background: white;
border-radius: 50%;
}
.progress-text {
position: relative;
font-family: Arial, sans-serif;
font-size: 10px;
color: #333;
z-index: 1;
}
</style>
<div class="pe-preserve-print vertical no-flex" style="position: relative;">
<div class="horizontal" id="block" :blink :outline="selected">
<div class="pe-no-print left-panel vertical" :error-invert="cell.type === 'code' && 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="!showProgress && cell.type === 'code'" :icon-size :icon :error="!!fn" @down.stop="run" :info-invert="cell?.autoRun" :success="!fn && !cell?.time" style="margin: 4px; border-radius: 50%;">
</oda-button>
<div ~if="showProgress && cell.type === 'code'" class="circular-progress-container" @down.stop="run">
<progress class="hidden-progress" max="100" :value="jupyter.progress"></progress>
<div class="circular-progress" :style="progressStyle">
<!-- <span class="progress-text">{{jupyter.progress}}%</span> -->
<oda-icon icon="av:stop" error style="border-radius: 50%;"></oda-icon>
</div>
</div>
<div ~if="cell.type === 'code'" >{{time}}</div>
<div ~if="cell.type === 'code'" >{{status}}</div>
</div>
</div>
<div class="vertical flex">
<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 && !editMode" :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>
<div id="outputs" ~if="outputs?.length && !cell?.hideCode" class="info border" flex style="z-index: 1;">
<oda-jupyter-outputs-toolbar :icon-size="iconSize * .7" :cell ~show="selected" :cell-control="this"></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 || $for.item.text" :row="$$for" :max="control?.maxRow" @down.stop @tap.stop></oda-jupyter-cell-out>
</div>
</div>
</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>
<oda-jupyter-divider></oda-jupyter-divider>
`,
set scrollCancel(n){
if (n){
this.async(()=>{
this.scrollCancel = false;
}, 500)
}
},
blink:{
$def: false,
set(n){
if(n && !this.fn){
this.jupyter.debounce('blink', ()=>{
this.blink = false;
}, 100)
}
}
},
scrollToCell(forward = undefined){
if(this.scrollCancel){
this.scrollCancel = false;
return;
}
switch (forward){
case true:{
if (this.jupyter.scrollTop + this.jupyter.offsetHeight/2 > this.offsetTop)
return;
if ((this.offsetTop + this.offsetHeight) >= Math.floor(this.jupyter.scrollTop + this.jupyter.offsetHeight)){
if(this.offsetHeight>this.jupyter.offsetHeight && this.previousElementSibling?.offsetHeight <this.jupyter.offsetHeight / 2){
this.previousElementSibling.scrollIntoView(true);
}
else
this.scrollIntoView(this.offsetHeight>this.jupyter.offsetHeight);
}
} break;
case false:{
if (this.jupyter.scrollTop + this.jupyter.offsetHeight/2 < this.offsetTop + this.offsetHeight)
return;
if (this.offsetTop <= Math.ceil(this.jupyter.scrollTop)){
if (this.previousElementSibling?.offsetHeight <this.jupyter.offsetHeight / 3)
this.previousElementSibling.scrollIntoView(true);
else
this.scrollIntoView(true);
}
} break;
default:{
this.scrollIntoView(true);
}
}
},
progressStyle() {
return`--progress: ${this.jupyter.progress}`
},
showProgress: false,
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 || '';
},
scrollToBlockEnd() {
this.async(() => {
let block = this.$('#block');
let visibleTop = this.jupyter.scrollTop,
visibleBottom = visibleTop + this.jupyter.offsetHeight,
blockBottom = this.offsetTop + block.offsetHeight;
if (blockBottom >= visibleTop && blockBottom <= visibleBottom)
return;
this.jupyter.scrollTop = blockBottom - this.jupyter.offsetHeight + 128;
})
},
async run(){
try{
// this.cell.hideOutput = false;
// this.cell.outputs = [];
// this.cell.controls = [];
this.blink = true;
this.showProgress = true;
this.checkBreakpoints();
this.scrollToBlockEnd();
let outs = this.$$('oda-jupyter-cell-out');
if (outs?.length) {
outs.forEach(i => i.showAll = false);
}
await this.auto_run();
}
finally {
this.notebook?.change();
this.cell?.next?.clearTimes();
this.showProgress = false;
this.scrollToBlockEnd();
this.blink = false;
}
},
async auto_run(autorun) {
if (this.fn) {
this.fn = null;
this.jupyter.stop = true;
return;
}
this.jupyter.stop = false;
// const task = ODA.addTask();
await new Promise(resolve =>{
this.async(async () => {
try {
for (let code of this.notebook.codes) {
if (code === this.cell) break;
if (code.time) continue;
const control = this.jupyter.getCell(code.id)?.control;
if (code.hideCode && !control.showProgress) continue;
control.checkBreakpoints();
await code.execute(control);
}
await this.cell.execute(this);
}
finally {
// ODA.removeTask(task);
resolve();
this.jupyter.progress = 0;
this.async(()=>{
this.$render();
})
}
}, 100)
})
// await this.$render();
// this.async(async () => {
// }, 50)
},
checkBreakpoints() {
let cell = this.cell;
cell.srcWithBreakpoints = '';
if (cell.hideCode || !cell.breakpoints)
return;
const control = this.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;
},
get output_height() {
return this.cell_height - this.control_height;
},
get control_height() {
return this.control.offsetHeight;
},
get cell_height() {
return this.offsetHeight;
},
fn: null,
$pdp: {
get jupyter(){
return this.domHost?.jupyter || this.domHost;
},
get icon(){
return this.fn? 'av:stop': '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.focusedCell === this.cell/* || this.focusedCell?.id === this.cell?.id*/);
}
},
get control() {
return this.$('#control') || undefined;
},
showAllOutputsRow: false,
outputsStep: 1,
},
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;
justify-content: center;
opacity: {{!visible?0:1}};
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 = (this.jupyter.isMoveCell || 0) + 1;
this.focusedCell = this.notebook.add(this.cell, key);
},
showInsertBtn() {
return !this.readOnly && JSON.parse(top._jupyterCellData || '[]')?.length;
},
insert() {
const cells = JSON.parse(top._jupyterCellData || '[]');
this.focusedCell = this.cell;
let lastCell;
cells.map(i => {
this.jupyter.isMoveCell = (this.jupyter.isMoveCell || 0) + 1;
lastCell = this.notebook.add(lastCell || this.cell, '', i);
lastCell.id = lastCell.metadata.id = getID();
})
this.focusedCell ||= lastCell;
top._jupyterCellData = undefined;
}
})
ODA({ is: 'oda-jupyter-toolbar', imports: '@tools/containers, @tools/property-grid',
template: `
<style>
:host{
position: sticky;
top: 20px;
z-index: {{cells.length + 2}};
}
.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">
<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 :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="iconEye" title="Hide/Show code" @tap.stop="toggleShowCode"></oda-button>
</div>
`,
get iconEye() {
return this.cell.hideCode ? 'bootstrap:eye-slash' : 'bootstrap:eye';
},
toggleShowCode() {
this.control.hideCode = this.cell.hideOutput = !this.cell.hideOutput;
this.jupyter.$render();
this.notebook.change();
},
move(direction){
let top = this.jupyter.scrollTop;
let id = this.cell.id;
if(direction<0){
top -= this.domHost.previousElementSibling.offsetHeight
}
else if(direction>0){
top += this.domHost.nextElementSibling.offsetHeight
}
this.jupyter.isMoveCell = (this.jupyter.isMoveCell || 0) + 1;
this.cell.move(direction);
this.jupyter.scrollTop = top;
this.async(() => {
this.jupyter.focusedCell = this.jupyter.getCell(id)?.cell;
}, 10)
},
cell: null,
iconSize: 16,
deleteCell() {
if (!window.confirm(`Delete cell?`)) return;
let id = null;
if (this.cell.prev) {
id = this.cell.prev.id;
}
else if(this.cell.next) {
id = this.cell.next.id;
}
this.jupyter.isMoveCell = (this.jupyter.isMoveCell || 0) + 1;
this.cell.delete();
this.async(() => {
this.jupyter.focusedCell = this.jupyter.getCell(id)?.cell;
}, 10)
},
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();
}
})
ODA({ is: 'oda-jupyter-outputs-toolbar',
template: `
<style>
:host{
opacity: .5;
position: sticky;
top: 20px;
z-index: 9;
display: block;
}
:host(:hover){
opacity: 1;
}
.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="carbon:up-to-top" title="scroll to Up" @tap="scrollUp"></oda-button>
<oda-button :icon-size icon="carbon:down-to-bottom" title="scroll to Down" @tap="scrollDown"></oda-button>
<oda-button :icon-size icon="icons:clear" @tap="clearOutputs" title="Clear outputs"></oda-button>
</div>
`,
cell: null,
cellControl: null,
iconSize: 16,
clearOutputs() {
this.cell.hideOutput = false;
this.cell.outputs = [];
this.cell.controls = [];
// this.domHost.scrollToCell();
},
scrollUp() {
this.parentElement.scrollIntoView({block: "start"});
},
scrollDown() {
const outs = this.cellControl.$$('oda-jupyter-cell-out');
if (outs?.length) {
outs.map(i => {
i.showAll = true;
})
}
this.async(() => {
this.parentElement.scrollIntoView({block: "end"});
}, 100)
}
})
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" 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 use-global-find :highlight-active-line="showCursor" :show-cursor></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.domHost.scrollCancel = true;
this.ace?.focus();
},
on_change_cursor(e) {
if (this.jupyter.isMoveCell)
return;
this.showCursor = true;
let range = this.ace?.getSelectionRange();
try{
if (this.cell.lastRange){
let currentRow = range.start.row;
let lastRow = this.cell.lastRange.start.row;
if (currentRow === lastRow) return;
this.throttle('scrollToCursor', () => {
this.scrollToCursor(currentRow);
}, 10)
}
}
finally {
this.cell.lastRange = range;
}
},
scrollToCursor(currentRow, shift = 1) {
let h = this.editor.lineHeight;
let lineTop = this.domHost.offsetTop + currentRow * h;
let lineBottom = this.domHost.offsetTop + (currentRow + 1) * h;
let visibleTop = this.jupyter.scrollTop;
let visibleBottom = this.jupyter.scrollTop + this.jupyter.offsetHeight;
let needScrollUp = lineTop < visibleTop + h;
let needScrollDown = lineBottom > visibleBottom - h;
if (needScrollUp) {
let targetScroll = Math.max(0, lineTop - h * shift);
this.jupyter.scrollTop = targetScroll;
} else if (needScrollDown) {
let targetScroll = lineBottom - this.jupyter.offsetHeight + h * shift;
this.jupyter.scrollTop = targetScroll;
}
},
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 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)
},
$listeners:{
resize(e){
this.async(()=>{
if(this.editor){
this.editor.gutterWidth = undefined;
this.editor?.editor?.resize?.();
}
})
}
},
$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();
}
},
get maxRow() {
// // $group: 'output',
// $pdp: true,
// $def: 50,
// get(){
// return this.cell?.readMetadata('maxRow', 50)
// },
// set(n){
// this.cell?.writeMetadata('maxRow', n)
// }
return this.maxOutputRows;
},
clearHiddenErros: {
$def: '',
get() {
let l = this.jupyter?.notebook?.data?.hiddenErrors?.length;
return l ? l + ' hidden errors' : '';
},