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.

368 lines (366 loc) 19.6 kB
let path = import.meta.url; let mdShowdown; ODA({ is: 'oda-md-viewer', imports: './dist/showdown.min.js, ./dist/decodeHTML.min.js, ./dist/highlight.min.js', template: ` <style> p { margin: {{pmargin || 'unset'}}; } :host { display: block; height: 100%; overflow-y: auto; line-height: 1.67;} table { border-collapse: collapse; width: 100%; margin-bottom: 16px;} th { @apply --header; border: 1px solid darkgray; padding: 2px; } td { border: 1px solid lightgray; padding: 2px; } img { max-width: 96%; height: auto; } blockquote { border-left: 10px solid #ccc; background: #f9f9f9; padding: 0.5em 10px; margin: 1.5em 10px; } code { background-color: #f8f8f8; border: 1px solid #dfdfdf; color: #333; font-family: Consolas,"Liberation Mono",Courier,monospace; font-weight: normal; padding: 0.125rem 0.3125rem 0.0625rem;} /*.katex-html { display: none; }*/ </style> <div ~html="html" style="margin: 2px 10px; white-space: normal;"></div> `, set srcmd(n) { this.setHTML(n); }, set src(n) { if (!this.srcmd) this.setHTML(n); }, html: '', $public: { pmargin: '' }, options: {}, defaultOptions: { omitExtraWLInCodeBlocks: false, noHeaderId: false, prefixHeaderId: false, rawPrefixHeaderId: false, ghCompatibleHeaderId: false, rawHeaderId: false, headerLevelStart: false, parseImgDimensions: false, simplifiedAutoLink: false, literalMidWordUnderscores: false, literalMidWordAsterisks: false, strikethrough: false, tables: true, tablesHeaderId: false, ghCodeBlocks: true, tasklists: false, smoothLivePreview: false, smartIndentationFix: false, disableForced4SpacesIndentedSublists: true, simpleLineBreaks: false, requireSpaceBeforeHeadingText: false, ghMentions: false, ghMentionsLink: 'https://github.com/{u}', encodeEmails: true, openLinksInNewWindow: true, backslashEscapesHTMLTags: true, emoji: true, underline: true, completeHTMLDocument: false, metadata: false, splitAdjacentBlockquotes: false }, async initShowdown() { if (!mdShowdown) // await import('./dist/showdown-katex.js'); // https://github.com/obedm503/showdown-katex await import('./dist/showdown-youtube.min.js'); mdShowdown = new showdown.Converter({ ...this.defaultOptions, ...this.options, extensions: ['youtube', // showdownKatex({ // delimiters: [ // { left: "$", right: "$", display: false }, // { left: '~', right: '~', display: false, asciimath: true }, // ], // }) , () => { return [{ type: "output", filter(text) { let left = "<pre><code\\b[^>]*>", right = "</code></pre>", flags = "g"; const replacement = (wholeMatch, match, left) => { let lang = (left.match(/class=\"([^ \"]+)/) || [])[1]; let html = lang && hljs.getLanguage(lang) ? hljs.highlight(lang, htmlDecode(match)).value : hljs.highlightAuto(htmlDecode(match)).value; let encode = htmlEncode(match); return `<oda-md-code code="${encode}" lang="${lang}"></oda-md-code>`; }; return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags); } }] }] }) }, async attached() { if (!this.src && !this.srcmd) { let url = new URL(window.location); let s = url.searchParams.get("lzs"); if (s) { const lzs = path.replaceAll('components/viewers/md-viewer/md-viewer.js', 'tools/jupyter/lib/lz-string.js'); if (!this.LZString) { const { LZString } = await import(lzs); this.LZString = LZString; } s = this.LZString.decompressFromEncodedURIComponent(s); this.setHTML('', s); } else { s = url.searchParams.get("s"); this.setHTML(s); } } }, async setHTML(s, lzs) { if (s || lzs) { await this.initShowdown(); let src = lzs || s; if (!lzs && (!this.srcmd && (s.endsWith('.md') || !(s.includes('~~~') || s.includes('```'))))) { try { src = await fetch(s); src = src && src.ok ? await src.text() : s; } catch (e) { } } this.source = src || s; // src = src.replace(/\$\$(.*)\$\$/g, `<h2 style="text-align: center;">$ $1 $</h2>`); src = src.replace('~~~~~_~~~~~', ''); src = src.replace(/(```\S*|~~~\S*)( +)/g, '$1' + '_'); this.html = mdShowdown.makeHtml(src); } } }) let files = new Map(); let fileCount = 0; ODA({ is: 'oda-md-code', imports: '@oda/icon, @oda/code-editor', template: ` <style> :host { transition: opacity .5s linear, visibility .5s linear; } .md { @apply --layout; display: block; padding: 4px; } .hljs { @apply --layout; display: block; padding: {{md ? '8px 4px 6px 10px;' : ''}}; height: {{_eh ? _eh + 'px' : ''}} } .hljs-comment, .hljs-quote { color: #93a1a1; } .hljs-keyword, .hljs-selector-tag, .hljs-addition { color: #859900; } .hljs-number, .hljs-string, hljs-meta .hljs-meta-string, .hljs-literal, .hljs-doctag, .hljs-regexp { color: #2aa198; } .hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #268bd2; } .hljs-attribute, .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-class .hljs-title, .hljs-type { color: #b58900; } .hljs-symbol, .hljs-bullet, .hljs-subst, .hljs-meta, .hljs-meta .hljs-keyword, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-link { color: #cb4b16; } .hljs-built_in, .hljs-deletion { color: #dc322f; } .hljs-formula { background: #eee8d5; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hjln { min-width:34px; color:gray; border-right:.1em solid; counter-reset: l; cursor:default; float:left; padding:8px 0; margin:0 0.5em 0 0; text-align:right; -moz-user-select:none; -webkit-user-select:none } .hjln span { counter-increment:l; display:block;padding:0 .5em 0 1em } .hjln span:before { content:counter(l) } .light { background-color: #ddd; display: inline-block;} .icon-info { display: flex; } .copy:active:before { position: absolute; right: 0; top: -26px; color: red; content:"copied"; } </style> <div class="flex vertical" ~style="{'margin-left': infpnl?'10px':''}" style=" position: relative;padding:0;color:darkgray"> <div ~if="showFilename" style="font-size:.8em;position: absolute; top:-4px;left:2px;">{{filename}}</div> <div style="cursor:pointer;position: absolute; top:18px;right:4px;display: flex"> <oda-icon ~if="showRun && showConsole" icon="device:developer-mode" icon-size="16" style="cursor:pointer; z-index: 9;" @tap="_switchConsole" title="show console"></oda-icon> <oda-icon class="copy" ~if="bcopy" icon="icons:content-copy" icon-size="16" style="cursor:pointer; z-index: 9;" @tap="_copy" title="copy to clipboard"></oda-icon> </div> <pre class="hljs" style="display:flex;border: .5px solid darkgray;border-radius:2px;overflow:auto;overflow-x:auto;min-height:32px" ~style="line? '' : 'white-space:pre-wrap;'+_getColor(true)"> <oda-icon class="icon-info" ~class="infpnl" ~style="_getColor" :icon="_getInfoIcon" style="position:absolute;left:-11px;top:22px;background:white; border-radius: 50%; z-index: 9" ~if="infpnl && !hideicon" :title="infpnl"></oda-icon> <code ~if="md" :contenteditable="enableEdit" ~class="md?'md':'hljs'" ~html="html" @input="_changed" style="outline:0px solid transparent;" ~style="md?'font-family:var(--font-family);font-size:.95em':'font-size:1.25em;line-height: 1.3em;'"></code> <oda-code-editor ~if="!md" :show-gutter="!hideGutter" :read-only="!enableEdit" :show-print-margin="false" :src="_code" style="flex: 1; background-color: white; opacity: unset !important; filter: unset !important;" font-size="16" :mode="_mode" @change="_changed" ~style="{'padding-left': hideGutter ? '10px' : '0', 'padding-top': hideGutter ? '16px' : 0 }" highlight-active-line="false" min-lines="1" :max-lines="_maxLines || 100" show-cursor=false @wheel.stop></oda-code-editor> </pre> <iframe ~if="showRun" :srcdoc="_src" style="padding: 6px;border-radius:0 0 2px 2px;border: 1px solid darkgray;min-width:0px;height:28px;margin:-14px 0 14px 0" onload="setTimeout(()=>{this.style.height=this?.contentDocument?.body?.scrollHeight+'px'},500)" ~style="{'min-height': (_h||28)+'px','max-height': _h ? _h+'px':'unset'}"></iframe> <div ~if="showRun && _showConsole" style="display:flex;margin-top:-14px"> <div style="font-size:.8em;">console:</div> <div style="flex:1"></div> <oda-icon icon="files:document" icon-size="12" style="z-index: 9; cursor:pointer;" @tap="_console=''" title="clear"></oda-icon> <oda-icon icon="icons:refresh" icon-size="16" style="z-index: 9; cursor:pointer;" @tap="_console='';this._changed(false)" title="refresh"></oda-icon> <oda-icon icon="icons:close" icon-size="16" style="z-index: 9; cursor:pointer;" @tap="_switchConsole" title="close"></oda-icon> </div> <textarea ~if="showRun && _showConsole" style="background-color:#eee;padding: 2px;border-radius:0 0 2px 2px;border: 1px solid darkgray;min-width:100%;max-width:100%;height:128px;" readonly>{{_console}}</textarea> </div> `, $public: { code: String, lang: String, html: '', _iframe: '', _code: '', hideicon: false, hideGutter: false, bcopy: true, _show: false, enableEdit: false, showRun: false, line: false, md: false, infpnl: '', filename: '', showFilename: false, components: [], _edit: '', _lang: '', _h: '', _eh: '', _src: '', _loadODA: false, showConsole: false, _showConsole: false, _console: '', warn: false, error: false, _mode: '', _maxLines: '100' }, $observers: { init(code, lang) { this._code = htmlDecode(code); if (this.md) { this.html = mdShowdown.makeHtml(this._code).replace(/<p>|<\/p>/g, ''); } else { this.html = this._code; } let n = lang; this._lang = lang; let _lang = ''; let langs = ['javascript', 'xml', 'xquery', 'css', 'http', 'ini', 'json', 'html', 'python']; langs.forEach(e => { if (this._lang.startsWith(e)) _lang = e; }) if (!_lang) { langs.forEach(e => { if (this._lang.includes(e)) _lang = e; }) } this._mode = _lang; if (this.filename) { this.showFilename = true; } else { fileCount += 1; this.filename = 'file_' + fileCount + this._lang.includes('html') ? '.html' : this._lang.includes('javascript') ? '.js' : ''; } this.bcopy = n.includes('copy') && !n.includes('nocopy'); this.enableEdit = n.includes('edit'); this.showRun = n.includes('run'); this.hideicon = n.includes('hideicon'); this.hideGutter = n.includes('hideGutter'); this.line = n.includes('line'); this.md = n.includes('md'); this.filename = n.includes('[') && n.includes(']') ? /(?:\[)(.*)(?:\])/g.exec(n)[1] : ''; this.components = n.includes('{') && n.includes('}') ? /(?:\{)(.*)(?:\})/g.exec(n)[1].split('_') : []; ['error', 'success', 'info', 'warning', 'help', 'like', 'faq'].forEach(e => { if (this._lang.includes(e)) this.infpnl = e }); this._h = /_h=(.*?)_/g.exec(n) ? /_h=(.*?)_/g.exec(n)[1] : ''; this._eh = /_eh=(.*?)_/g.exec(n) ? /_eh=(.*?)_/g.exec(n)[1] : ''; this._maxLines = /_maxLines=(.*?)_/g.exec(n) ? /_maxLines=(.*?)_/g.exec(n)[1] : '100'; this._loadODA = n.includes('loadoda'); this.showConsole = n.includes('console'); this.warn = n.includes('warn'); this.error = n.includes('error'); if (this.md) { this.html = mdShowdown.makeHtml(this._code).replace(/<p>|<\/p>/g, ''); } else { this.html = this._code; } files.set(this.filename, this._code); this._getIframe(); if (this.components?.length) { document.addEventListener("iframeChanged", e => { if (this._lang !== e.detail._lang && this.components.includes(e.detail.filename)) { this._changed(false); } }); } } }, _switchConsole() { this._showConsole = !this._showConsole; if (this._showConsole) this._changed(false); else this._console = ''; }, _setConsole(e) { if (this._showConsole && this.$('iframe')) { const _cons = (e) => { if (!this.warn && e.startsWith('console.warn > ')) return; if (!this.error && e.startsWith('console.error > ')) return; this._console += '\r\n' + e; if (this.$('textarea')) { let textarea = this.$('textarea'); this.async(() => { textarea.scrollTop = textarea.scrollHeight; }) } } let cons = (function(oldCons) { return { log: function(text) { oldCons.log(text); if (text.toString().startsWith('from alert')) _cons(text); else _cons('console.log > ' + text); }, info: function(text) { oldCons.info(text); _cons('console.info > ' + text); }, warn: function(text) { oldCons.warn(text); _cons('console.warn > ' + text); }, error: function(text) { oldCons.error(text); _cons('console.error > ' + text); } } }(this.$('iframe').contentWindow.console)); this.$('iframe').contentWindow.console = cons; } }, async _copy() { await navigator.clipboard.writeText(this._code); }, _changed(iframeChanged = true) { const code = this.$('code') || this.$('oda-code-editor'); if (!code) return; this._edit = code.editor?.getValue() || code.innerText || ''; files.set(this.filename, this._edit); this._getIframe(); if (iframeChanged && this.filename) document.dispatchEvent(new CustomEvent('iframeChanged', { detail: { _lang: this._lang, filename: this.filename } })); }, _getIframe() { if (!this.showRun || !this._code) return; let css = ''; let code = files.get(this.filename) && files.get(this.filename); if (this.filename.includes('.js') || this._lang.includes('javascript')) { if (this.components && this.components.length) { for (const c of this.components) { if (c.endsWith('.css')) { css += files.get(c) || ''; } else { let v = files.get(c); code = v + code; } } } code = code.replace(/import .*oda.js\'/g, ''); code = code.replace(/import .*(\'|\")\//g, `import $1${ODA.rootPath}/`); code = code.replace(/(<oda-.*=\'|\")\//g, `$1${ODA.rootPath}/`); this._iframe = ` ${css} ${this._scriptODA()} <script type="module"> ${code} </script> ${this.filename.includes('.js') ? `<${this.filename.replace('.js', '')}></${this.filename.replace('.js', '')}>` : ''} `; } else if (this.filename && this.filename.includes('.html') || this._lang.includes('html')) { code = code.replace(/\<script.*src="\//g, `<script type="module" src="${ODA.rootPath}/`); code = code.replace(/\<script.*oda.js"\>\<\/script\>/g, this._scriptODA()); if (this.components && this.components.length) { let v = '<script type="module">'; for (const c of this.components) { if (c.endsWith('.css')) { css += files.get(c) || ''; } else { v += files.get(c) && files.get(c); } } if (!code.includes('/oda.js"></script>')) code = ` ${css} ${this._scriptODA()}` + code; code += v + '</script>'; } this._iframe = code; } this._iframe = this._iframe.replace(/alert\(/g, 'console.log("from alert > " + '); this._iframe = `<meta charset="UTF-8">` + this._iframe; this._setConsole(); this._src = this._iframe; }, _getInfoIcon() { return { 'error': 'icons:add-circle-outline:45', 'success': 'enterprise:check-box', 'info': 'icons:info-outline', 'warning': 'icons:warning', 'help': 'icons:help-outline', 'faq': 'icons:question-answer', 'like': 'shopping:like' }[this.infpnl]; }, _getColor(left = false) { let color = { 'error': 'red !important', 'success': 'green !important', 'info': 'blueviolet !important', 'warning': 'orange !important', 'help': 'blue !important', 'faq': 'brown !important', 'like': 'limegreen !important' }[this.infpnl]; if (!color || !this.infpnl) return ''; if (!left) return `fill:${color};border-color:${color}`; return `border-left-color:${color};border-left-width:3px`; }, _scriptODA() { if (this._loadODA) return `<script type="module" src="${ODA.rootPath + '/oda.js'}"></script>`; return `<script type="module" src="${ODA.rootPath + '/oda.js'}"></script>`; } })