mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Merge pull request #28978 from overleaf/mj-visual-editor-broken-image
[web] Add image loading fallback screen to visual editor GitOrigin-RevId: 205c6355011802672e57123bb2a61cb708242e31
This commit is contained in:
committed by
Copybot
parent
fe3ace423c
commit
f694da4443
@@ -272,6 +272,7 @@
|
||||
"clear_search": "",
|
||||
"clearing": "",
|
||||
"click_here_to_view_sl_in_lng": "",
|
||||
"click_recompile_and_check_your_pdf_to_see_how_its_looking": "",
|
||||
"click_to_unpause": "",
|
||||
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "",
|
||||
"clone_with_git": "",
|
||||
@@ -1837,6 +1838,7 @@
|
||||
"the_primary_file_for_compiling_your_project": "",
|
||||
"the_target_folder_could_not_be_found": "",
|
||||
"the_version_of_tex_live_used_for_compiling": "",
|
||||
"the_visual_editor_cant_preview_this_type_of_image_file": "",
|
||||
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "",
|
||||
"their_projects_will_be_transferred_to_another_user": "",
|
||||
"then_x_price_per_month": "",
|
||||
|
||||
@@ -26,6 +26,15 @@ export const visualHighlightStyle = syntaxHighlighting(
|
||||
])
|
||||
)
|
||||
|
||||
const graphicsErrorColorTheme = EditorView.baseTheme({
|
||||
'&dark .ol-cm-graphics-loading-error': {
|
||||
'--graphics-loading-error-color': 'var(--content-placeholder-dark)',
|
||||
},
|
||||
'&light .ol-cm-graphics-loading-error': {
|
||||
'--graphics-loading-error-color': 'var(--content-placeholder)',
|
||||
},
|
||||
})
|
||||
|
||||
const mainVisualTheme = EditorView.theme({
|
||||
'&.cm-editor': {
|
||||
'--visual-font-family':
|
||||
@@ -351,6 +360,30 @@ const mainVisualTheme = EditorView.theme({
|
||||
border: '1px solid red',
|
||||
padding: '8px',
|
||||
},
|
||||
'.ol-cm-graphics-loading-error': {
|
||||
color: 'var(--graphics-loading-error-color)',
|
||||
minHeight: '300px',
|
||||
textAlign: 'left',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: 'var(--spacing-03)',
|
||||
padding: '0 var(--spacing-10)',
|
||||
maxWidth: '500px',
|
||||
margin: '0 auto',
|
||||
},
|
||||
'.ol-cm-graphics-loading-error-title, .ol-cm-graphics-loading-error-subtitle':
|
||||
{
|
||||
display: 'block',
|
||||
},
|
||||
'.ol-cm-graphics-loading-error-title': {
|
||||
fontSize: 'var(--font-size-04)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
'.ol-cm-graphics-loading-error-subtitle': {
|
||||
fontSize: 'var(--font-size-02)',
|
||||
},
|
||||
'.ol-cm-environment-centered .ol-cm-graphics': {
|
||||
margin: '0 auto',
|
||||
},
|
||||
@@ -491,5 +524,6 @@ const contentWidthSetter = EditorView.updateListener.of(update => {
|
||||
export const visualTheme: Extension = [
|
||||
contentWidthThemeConf.of(createContentWidthTheme('100%')),
|
||||
mainVisualTheme,
|
||||
graphicsErrorColorTheme,
|
||||
contentWidthSetter,
|
||||
]
|
||||
|
||||
+27
-1
@@ -122,6 +122,7 @@ export class GraphicsWidget extends WidgetType {
|
||||
}
|
||||
|
||||
createImage(view: EditorView, url: string) {
|
||||
const wrapper = document.createElement('div')
|
||||
const image = document.createElement('img')
|
||||
image.classList.add('ol-cm-graphics')
|
||||
image.classList.add('ol-cm-graphics-loading')
|
||||
@@ -135,8 +136,33 @@ export class GraphicsWidget extends WidgetType {
|
||||
this.height = image.height // for estimatedHeight
|
||||
view.requestMeasure()
|
||||
})
|
||||
image.addEventListener('error', () => {
|
||||
const errorElement = this.createErrorElement(view)
|
||||
wrapper.replaceChildren(errorElement)
|
||||
this.height = wrapper.clientHeight
|
||||
view.requestMeasure()
|
||||
})
|
||||
|
||||
return image
|
||||
wrapper.appendChild(image)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
createErrorElement(view: EditorView): HTMLElement {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.classList.add('ol-cm-graphics-loading-error')
|
||||
const title = document.createElement('span')
|
||||
title.classList.add('ol-cm-graphics-loading-error-title')
|
||||
title.textContent = view.state.phrase(
|
||||
'the_visual_editor_cant_preview_this_type_of_image_file'
|
||||
)
|
||||
const subtitle = document.createElement('span')
|
||||
subtitle.classList.add('ol-cm-graphics-loading-error-subtitle')
|
||||
subtitle.textContent = view.state.phrase(
|
||||
'click_recompile_and_check_your_pdf_to_see_how_its_looking'
|
||||
)
|
||||
wrapper.appendChild(title)
|
||||
wrapper.appendChild(subtitle)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
async renderPDF(view: EditorView, canvas: HTMLCanvasElement, url: string) {
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"clearing": "Clearing",
|
||||
"click_here_to_view_sl_in_lng": "Click here to use __appName__ in <0>__lngName__</0>",
|
||||
"click_link_to_proceed": "Click <b>__clickText__</b> below to proceed.",
|
||||
"click_recompile_and_check_your_pdf_to_see_how_its_looking": "Click Recompile and check your PDF to see how it’s looking.",
|
||||
"click_to_unpause": "Click to unpause and reactivate your Overleaf premium features.",
|
||||
"clicking_delete_will_remove_sso_config_and_clear_saml_data": "Clicking <0>Delete</0> will remove your SSO configuration and unlink all users. You can only do this when SSO is disabled in your group settings.",
|
||||
"clone_with_git": "Clone with Git",
|
||||
@@ -2353,6 +2354,7 @@
|
||||
"the_supplied_uri_is_invalid": "The link to open this content on Overleaf included an invalid URI. If this keeps happening for links on a particular site, please report this to them.",
|
||||
"the_target_folder_could_not_be_found": "The target folder could not be found.",
|
||||
"the_version_of_tex_live_used_for_compiling": "The version of TeX Live used for compiling",
|
||||
"the_visual_editor_cant_preview_this_type_of_image_file": "The Visual Editor can’t preview this type of image file.",
|
||||
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "The width you choose here is based on the width of the text in your document. Alternatively, you can customize the image size directly in the LaTeX code.",
|
||||
"their_projects_will_be_transferred_to_another_user": "Their projects will all be transferred to another user of your choice",
|
||||
"theme": "Theme",
|
||||
|
||||
+18
-15
@@ -11,6 +11,7 @@ import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-pat
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
import { base64image } from '../fixtures/image'
|
||||
|
||||
const clickToolbarButton = (text: string) => {
|
||||
cy.findByLabelText(text).click()
|
||||
@@ -64,7 +65,7 @@ describe('<FigureModal />', function () {
|
||||
previewByPath: cy
|
||||
.stub()
|
||||
.as('previewByPath')
|
||||
.returns({ url: 'frog.jpg', extension: 'jpg' }),
|
||||
.returns({ url: base64image, extension: 'png' }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -631,20 +632,22 @@ text below`,
|
||||
})
|
||||
|
||||
it('Opens figure modal on pasting image', function () {
|
||||
cy.fixture<Uint8Array>('images/gradient.png').then(gradientBuffer => {
|
||||
const gradientFile = new File([gradientBuffer], 'gradient.png', {
|
||||
type: 'image/png',
|
||||
})
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.items.add(gradientFile)
|
||||
cy.wrap(clipboardData.files).should('have.length', 1)
|
||||
cy.get('.cm-content').trigger('paste', { clipboardData })
|
||||
cy.findByText('Upload from computer').should('be.visible')
|
||||
cy.findByLabelText('File name in this project').should(
|
||||
'have.value',
|
||||
'gradient.png'
|
||||
)
|
||||
})
|
||||
cy.fixture<Uint8Array<ArrayBuffer>>('images/gradient.png').then(
|
||||
gradientBuffer => {
|
||||
const gradientFile = new File([gradientBuffer], 'gradient.png', {
|
||||
type: 'image/png',
|
||||
})
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.items.add(gradientFile)
|
||||
cy.wrap(clipboardData.files).should('have.length', 1)
|
||||
cy.get('.cm-content').trigger('paste', { clipboardData })
|
||||
cy.findByText('Upload from computer').should('be.visible')
|
||||
cy.findByLabelText('File name in this project').should(
|
||||
'have.value',
|
||||
'gradient.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: Add tests for replacing image when we can match on image path
|
||||
|
||||
+61
-5
@@ -10,6 +10,7 @@ import { mockScope } from '../helpers/mock-scope'
|
||||
import forEach from 'mocha-each'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { base64image } from '../fixtures/image'
|
||||
|
||||
describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
beforeEach(function () {
|
||||
@@ -34,7 +35,16 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
previewByPath: cy
|
||||
.stub()
|
||||
.as('previewByPath')
|
||||
.callsFake(path => ({ url: path, extension: 'png' })),
|
||||
.callsFake(path =>
|
||||
path === 'valid.png'
|
||||
? { url: base64image, extension: 'png' }
|
||||
: path === 'graphic.eps'
|
||||
? {
|
||||
url: 'data:application/postscript,0 0 moveto (hello) show',
|
||||
extension: 'eps',
|
||||
}
|
||||
: null
|
||||
),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -256,11 +266,11 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
})
|
||||
|
||||
it('loads figures', function () {
|
||||
cy.get('@third-line').type('path/to/image')
|
||||
cy.get('@third-line').type('valid.png')
|
||||
|
||||
cy.get('@third-line').should(
|
||||
'contain.text',
|
||||
' \\includegraphics[width=0.5\\linewidth]{path/to/image}'
|
||||
' \\includegraphics[width=0.5\\linewidth]{valid.png}'
|
||||
)
|
||||
|
||||
// move the cursor out of the figure
|
||||
@@ -269,10 +279,56 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||
// Should be removed from dom when line is hidden
|
||||
cy.get('.cm-content').should(
|
||||
'not.contain.text',
|
||||
'\\includegraphics[width=0.5\\linewidth]{path/to/image}'
|
||||
'\\includegraphics[width=0.5\\linewidth]{valid.png}'
|
||||
)
|
||||
|
||||
cy.get('img.ol-cm-graphics').should('have.attr', 'src', 'path/to/image')
|
||||
cy.get('img.ol-cm-graphics').should('have.attr', 'src', base64image)
|
||||
})
|
||||
|
||||
it('shows error state for figures we cannot render', function () {
|
||||
cy.get('@third-line').type('graphic.eps')
|
||||
|
||||
cy.get('@third-line').should(
|
||||
'contain.text',
|
||||
' \\includegraphics[width=0.5\\linewidth]{graphic.eps}'
|
||||
)
|
||||
|
||||
// move the cursor out of the figure
|
||||
cy.get('@third-line').type('{DownArrow}{DownArrow}{DownArrow}{DownArrow}')
|
||||
|
||||
// Should be removed from dom when line is hidden
|
||||
cy.get('.cm-content').should(
|
||||
'not.contain.text',
|
||||
'\\includegraphics[width=0.5\\linewidth]{graphic.eps}'
|
||||
)
|
||||
|
||||
// Should show error state
|
||||
cy.get('div.ol-cm-graphics-loading-error').should('be.visible')
|
||||
cy.findByText(
|
||||
/The Visual Editor can’t preview this type of image file./
|
||||
).should('be.visible')
|
||||
})
|
||||
|
||||
it('shows error state for missing figures', function () {
|
||||
cy.get('@third-line').type('missing.png')
|
||||
|
||||
cy.get('@third-line').should(
|
||||
'contain.text',
|
||||
' \\includegraphics[width=0.5\\linewidth]{missing.png}'
|
||||
)
|
||||
|
||||
// move the cursor out of the figure
|
||||
cy.get('@third-line').type('{DownArrow}{DownArrow}{DownArrow}{DownArrow}')
|
||||
|
||||
// Should be removed from dom when line is hidden
|
||||
cy.get('.cm-content').should(
|
||||
'not.contain.text',
|
||||
'\\includegraphics[width=0.5\\linewidth]{missing.png}'
|
||||
)
|
||||
|
||||
// Should show error state
|
||||
cy.get('div.ol-cm-graphics-error').should('be.visible')
|
||||
cy.findByText(/missing.png/).should('be.visible')
|
||||
})
|
||||
|
||||
it('marks lines as figure environments', function () {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const base64image =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpaJVETuIOGSogmBBVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIq4uToouU+L+k0CLWg+N+vLv3uHsHCNUi06y2cUDTbTMRi4qp9KoYeIWAPnShB6Mys4w5SYqj5fi6h4+vdxGe1frcn6NbzVgM8InEs8wwbeIN4ulN2+C8TxxieVklPiceM+mCxI9cVzx+45xzWeCZITOZmCcOEYu5JlaamOVNjXiKOKxqOuULKY9VzluctWKZ1e/JXxjM6CvLXKc5hBgWsQQJIhSUUUARNiK06qRYSNB+tIV/0PVL5FLIVQAjxwJK0CC7fvA/+N2tlZ2c8JKCUaD9xXE+hoHALlCrOM73sePUTgD/M3ClN/ylKjDzSXqloYWPgN5t4OK6oSl7wOUOMPBkyKbsSn6aQjYLvJ/RN6WB/lugc83rrb6P0wcgSV3Fb4CDQ2AkR9nrLd7d0dzbv2fq/f0ARfNylZJUgMQAAAAGYktHRABuAP8AAGHZRr4AAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnAhELEhgyPeVkAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAyVJREFUeNrt1rEJgDAURVGVNCmS2hS6PziCteIYWjuEbiEfOWeEV1xe35bt6QhjnlYjBJLzbYRABhMAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBbwX6m13QqB5HwbIZBSLyN4WACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBfCFVMtphUBKvYwQSBsPI3hYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIF8IUXjtUMuBMh1xAAAAAASUVORK5CYII='
|
||||
Reference in New Issue
Block a user