Skip to content

Commit

Permalink
[gem-book] Improve sandpack
Browse files Browse the repository at this point in the history
- undo
- support safari
- reset
- bundle size
  • Loading branch information
mantou132 committed Sep 28, 2024
1 parent f6c999c commit 83085a6
Show file tree
Hide file tree
Showing 6 changed files with 3,124 additions and 4,975 deletions.
1 change: 1 addition & 0 deletions packages/gem-book/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"sitemap-rspack-plugin": "^1.1.1",
"string-replace-loader": "^3.1.0",
"ts-loader": "^9.5.1",
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"yaml": "^1.10.2"
},
Expand Down
1 change: 1 addition & 0 deletions packages/gem-book/src/bin/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export async function buildApp(dir: string, options: Required<CliUniqueConfig>,
compilerOptions: {
module: 'esnext',
declarationMap: false,
importHelpers: true,
},
},
},
Expand Down
154 changes: 78 additions & 76 deletions packages/gem-book/src/element/elements/pre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ let contenteditableValue = 'true';
if (div.isContentEditable) contenteditableValue = 'plaintext-only';
})();

const supportAnchor = CSS.supports('anchor-name: --foo');

// https://github.com/PrismJS/prism/blob/master/plugins/autoloader/prism-autoloader.js
const langDependencies: Record<string, string | string[]> = {
javascript: 'clike',
Expand Down Expand Up @@ -284,29 +286,51 @@ const styles = createCSSSheet(css`
--keyword-color: var(--code-keyword-color, #93219e);
--attribute-color: var(--code-attribute-color, #4646c6);
}
.gem-code,
.code-container,
.linenumber {
padding: ${PADDING};
box-sizing: border-box;
min-height: 100%;
height: max-content;
}
.gem-code {
.padding {
padding: ${PADDING};
box-sizing: border-box;
}
.code-container {
min-width: 0;
flex-grow: 1;
overflow-x: auto;
overflow-clip-box: content-box;
scrollbar-width: thin;
display: flex;
position: relative;
}
.gem-code,
.code-shadow {
font-family: inherit;
flex-grow: 1;
text-align: left;
white-space: pre;
tab-size: 2;
hyphens: none;
overflow-clip-box: content-box;
box-shadow: none;
border: none;
background: transparent;
outline: none;
caret-color: ${theme.textColor};
}
.gem-code {
anchor-name: --code;
}
.code-shadow {
position: absolute;
position-anchor: --code;
inset: anchor(top) anchor(right) anchor(bottom) anchor(left);
color: transparent;
}
@supports not (anchor-name: --foo) {
.code-shadow {
display: none;
}
}
.linenumber {
display: inline-flex;
flex-direction: column;
Expand Down Expand Up @@ -445,7 +469,12 @@ export class Pre extends GemElement {
return !this.filename || this.headless;
}

get #contentElement() {
return supportAnchor ? this.#editorRef.element : this.#codeRef.element;
}

#codeRef = createRef<HTMLElement>();
#editorRef = createRef<HTMLElement>();

#ranges: number[][];
#highlightLineSet: Set<number>;
Expand Down Expand Up @@ -488,59 +517,8 @@ export class Pre extends GemElement {
this.#onInputHandle();
};

// https://stackoverflow.com/questions/62054839/shadowroot-getselection
#chrome = 'getSelection' in this.shadowRoot!;

#offset = 0;
#onInputHandle = () => {
let focusNode: Node | null;
let focusOffset = 0;
if (this.#chrome) {
const selection: Selection = (this.shadowRoot as any).getSelection();
focusNode = selection?.focusNode;
focusOffset = selection?.focusOffset;
} else {
const range: Range = (getSelection() as any)?.getComposedRanges(this.shadowRoot)?.at(0);
focusNode = range?.startContainer;
focusOffset = range?.startOffset;
}
if (focusNode?.nodeType !== Node.TEXT_NODE) return;
this.#offset = 0;
const textNodeIterator = document.createNodeIterator(this.#codeRef.element!, NodeFilter.SHOW_TEXT);
while (true) {
const textNode = textNodeIterator.nextNode();
if (textNode && textNode !== focusNode) {
this.#offset += textNode.nodeValue!.length;
} else {
this.#offset += focusOffset;
break;
}
}
const content = this.#codeRef.element!.textContent!;
this.textContent = content;
};

#setOffset = () => {
if (!this.editable || !this.#offset) return;
const textNodeIterator = document.createNodeIterator(this.#codeRef.element!, NodeFilter.SHOW_TEXT);
let offset = this.#offset;
while (true) {
const textNode = textNodeIterator.nextNode();
if (!textNode) break;
if (textNode.nodeValue!.length < offset) {
offset -= textNode.nodeValue!.length;
} else {
const sel = window.getSelection();
if (!sel) break;
const range = document.createRange();
range.setStart(textNode, offset);
range.collapse(false);
// TODO: safari not work
sel.removeAllRanges();
sel.addRange(range);
break;
}
}
this.textContent = this.#contentElement!.textContent;
};

#isVisble = false;
Expand Down Expand Up @@ -574,24 +552,35 @@ export class Pre extends GemElement {
lineNumbersParts[i].at(0)! - 1
} @@</span>${c}`,
);
this.#setOffset();
};

@mounted()
#init = () => {
const ob = new MutationObserver(() => this.update());
#initContent = () => {
if (this.#contentElement) {
this.#contentElement.textContent = this.textContent;
}
};

@mounted()
#obTextContent = () => {
const ob = new MutationObserver(() => {
this.#initContent();
this.update();
});
ob.observe(this, { childList: true, characterData: true, subtree: true });
return () => ob.disconnect();
};

@mounted()
#obVisibity = () => {
const io = new IntersectionObserver((entries) => {
for (const { intersectionRatio } of entries) {
this.#isVisble = intersectionRatio > 0;
this.update();
}
});
io.observe(this);
return () => {
io.disconnect();
ob.disconnect();
};
return () => io.disconnect();
};

render() {
Expand All @@ -615,7 +604,7 @@ export class Pre extends GemElement {
)}
${this.#linenumber
? html`
<div class="linenumber">
<div class="linenumber padding">
${lineNumbersParts.map(
(numbers, index, arr) => html`
${numbers.map((n) => html`<span>${n}</span>`)}${arr.length - 1 !== index
Expand All @@ -626,16 +615,29 @@ export class Pre extends GemElement {
</div>
`
: ''}
<code
ref=${this.#codeRef.ref}
class="gem-code"
spellcheck="false"
contenteditable=${this.editable ? contenteditableValue : false}
@compositionstart=${this.#compositionstartHandle}
@compositionend=${this.#compositionendHandle}
@input=${this.#onInput}
>${parts.join('\n'.repeat(IGNORE_LINE + 1))}</code
>
<div class="code-container">
<code
ref=${this.#codeRef.ref}
class="gem-code padding"
spellcheck="false"
contenteditable=${this.editable ? contenteditableValue : false}
@compositionstart=${this.#compositionstartHandle}
@compositionend=${this.#compositionendHandle}
@input=${this.#onInput}
>${parts.join('\n'.repeat(IGNORE_LINE + 1))}</code
>
${this.editable
? html`<code
ref=${this.#editorRef.ref}
class="code-shadow padding"
spellcheck="false"
contenteditable=${this.editable ? contenteditableValue : false}
@compositionstart=${this.#compositionstartHandle}
@compositionend=${this.#compositionendHandle}
@input=${this.#onInput}
></code>`
: ''}
</div>
</div>
`;
}
Expand Down
51 changes: 30 additions & 21 deletions packages/gem-book/src/plugins/sandpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
import type { GemBookElement } from '../element';
import type { Pre } from '../element/elements/pre';

const ESBUILD_URL = 'https://esm.sh/esbuild-wasm';
const ESBUILD_URL = 'https://esm.sh/esbuild-wasm@0.24.0';
const CSB_URL = 'https://codesandbox.io/api/v1/sandboxes/define?json=1';
const SANDPACK_CLIENT_ESM = 'https://esm.sh/@codesandbox/sandpack-client@2.18.2?bundle';
const LZ_STRING_ESM = 'https://esm.sh/lz-string@1.5.0';
Expand Down Expand Up @@ -164,6 +164,12 @@ const styles = createCSSSheet(css`
border-radius: ${theme.normalRound};
color-scheme: light;
}
.sandbox.loading {
/* Safari not load hidden iframe */
position: absolute;
width: 1px;
opacity: 0;
}
@container (max-width: 700px) {
.container {
grid-template: 'tabs' 'code' 'preview' / 100%;
Expand Down Expand Up @@ -210,18 +216,19 @@ class _GbpSandpackElement extends GemBookPluginElement {
get #indexTemplate() {
const style = getComputedStyle(document.body);
return `
<style>
body {
font: ${style.font};
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
app-root:not(:defined) {
display: contents;
}
</style>
<app-root id=root></app-root>
`.trim();
<!DOCTYPE html>
<style>
body {
font: ${style.font};
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
app-root:not(:defined) {
display: contents;
}
</style>
<app-root id=root></app-root>
`.trim();
}

get #useESMBuild() {
Expand Down Expand Up @@ -317,7 +324,7 @@ class _GbpSandpackElement extends GemBookPluginElement {
};
`,
)}`;
return ['https://cdn.jsdelivr.net/npm/eruda', erudaInit];
return ['https://cdn.jsdelivr.net/npm/eruda@3.3.0', erudaInit];
}

// 兼容 sandpack
Expand Down Expand Up @@ -405,7 +412,10 @@ class _GbpSandpackElement extends GemBookPluginElement {
compile();
},
dispatch: ({ type }: SandpackMessage) => {
if (type === 'refresh') this.#state({ status: 'done' });
switch (type) {
case 'refresh':
return this.#iframeRef.element?.contentWindow?.location.reload();
}
},
destroy: () => URL.revokeObjectURL(iframe.src),
} as SandpackClient;
Expand All @@ -427,8 +437,8 @@ class _GbpSandpackElement extends GemBookPluginElement {
'index.html': { code: this.#indexTemplate },
[this.#defaultEntryFilename]: { code: '' },
} as SandpackBundlerFiles),
dependencies: this.#dependencies,
entry: this.#entry,
dependencies: this.#dependencies,
},
{
showOpenInCodeSandbox: false,
Expand Down Expand Up @@ -462,10 +472,9 @@ class _GbpSandpackElement extends GemBookPluginElement {

#onReset = async () => {
const client = await this.#sandpackClient;
if (client) {
this.#state({ status: 'initialization' });
client.dispatch({ type: 'refresh' });
}
if (!client) return;
this.#state({ status: 'initialization' });
client.dispatch({ type: 'refresh' });
};

#onFork = async () => {
Expand Down Expand Up @@ -576,7 +585,7 @@ class _GbpSandpackElement extends GemBookPluginElement {
<div class="sandbox" ?hidden=${status === 'done'}>
<span class="status">${status.replace(/_|-/g, ' ')}...</span>
</div>
<iframe class="sandbox" ref=${this.#iframeRef.ref} ?hidden=${status !== 'done'}></iframe>
<iframe ref=${this.#iframeRef.ref} class=${classMap({ sandbox: true, loading: status !== 'done' })}></iframe>
</div>
</div>
`;
Expand Down
2 changes: 1 addition & 1 deletion packages/gem/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class RefObject<T = HTMLElement> {
.map((e) => [...e.querySelectorAll(this.refSelector)] as T[])
.flat();
}
toString() {
[Symbol.toPrimitive]() {
return this.ref;
}
}
Expand Down
Loading

0 comments on commit 83085a6

Please sign in to comment.