diff --git a/docs/userGuide/syntax/annotations.md b/docs/userGuide/syntax/annotations.md index f2364998bf..deafa1a6d8 100644 --- a/docs/userGuide/syntax/annotations.md +++ b/docs/userGuide/syntax/annotations.md @@ -14,7 +14,7 @@ The x and y coordinates of each Annotate Point are relative to the image and are html - + @@ -191,11 +191,12 @@ Here we showcase some use cases of the Annotate feature. This is effectively the same as the options used for the [picture](#pictures) component. | Name | Type | Default | Description | -| ------ | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------| --------- | ------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | alt | `string` | | **This must be specified.**
The alternative text of the image. | | src | `string` | | **This must be specified.**
The URL of the image.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_ | | height | `string` |`''`| The height of the image in pixels. | | width | `string` |`''`| The width of the image in pixels.
If both width and height are specified, width takes priority over height. It is to maintain the image's aspect ratio. | +| lazy | `boolean` | false | The `` component lazy loads if this attribute is specified.
**Either the height or width should be specified to avoid layout shifts while lazy loading images.** | diff --git a/docs/userGuide/syntax/pictures.md b/docs/userGuide/syntax/pictures.md index 22ed0fddaf..f5bfde2ac8 100644 --- a/docs/userGuide/syntax/pictures.md +++ b/docs/userGuide/syntax/pictures.md @@ -5,7 +5,7 @@ html - + MarkBind Logo @@ -18,11 +18,12 @@ alt | `string` | | **This must be specified.**
The alternative text of the im height | `string` | | The height of the image in pixels. src | `string` | | **This must be specified.**
The URL of the image.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_ width | `string` | | The width of the image in pixels.
If both width and height are specified, width takes priority over height. It is to maintain the image's aspect ratio. +lazy | `boolean` | false | The `` component lazy loads if this attribute is specified.
**Either the height or width should be specified to avoid layout shifts while lazy loading images.**
```html - + MarkBind Logo ``` diff --git a/packages/core/src/html/NodeProcessor.ts b/packages/core/src/html/NodeProcessor.ts index b933a28b42..b211d7168d 100644 --- a/packages/core/src/html/NodeProcessor.ts +++ b/packages/core/src/html/NodeProcessor.ts @@ -265,6 +265,20 @@ export class NodeProcessor { */ if (!_.has(node.attribs, 'v-pre')) { node.attribs['v-pre'] = ''; } break; + case 'pic': + case 'annotate': + if (_.has(node.attribs, 'lazy') + && !(_.has(node.attribs, 'width') || _.has(node.attribs, 'height'))) { + const filePath = context.callStack.length > 0 ? context.callStack[context.callStack.length - 1] + : context.cwf; + logger.warn( + `${filePath} --- ` + + 'Both width and height are not specified when using lazy loading in the file and' + + ' it might cause shifting in page layouts. ' + + 'To ensure proper functioning of lazy loading, please specify either one or both.\n', + ); + } + break; default: break; } diff --git a/packages/core/test/unit/html/NodeProcessor.test.ts b/packages/core/test/unit/html/NodeProcessor.test.ts index 69b71e2057..10505ccd2c 100644 --- a/packages/core/test/unit/html/NodeProcessor.test.ts +++ b/packages/core/test/unit/html/NodeProcessor.test.ts @@ -1,8 +1,9 @@ import path from 'path'; import cheerio from 'cheerio'; import htmlparser from 'htmlparser2'; -import * as testData from './NodeProcessor.data'; +import { expect } from '@jest/globals'; import * as logger from '../../../src/utils/logger'; +import * as testData from './NodeProcessor.data'; import { Context } from '../../../src/html/Context'; import { shiftSlotNodeDeeper, transformOldSlotSyntax } from '../../../src/html/vueSlotSyntaxProcessor'; import { getNewDefaultNodeProcessor } from '../utils/utils'; @@ -139,6 +140,68 @@ test('processNode processes dropdown with header slot taking priority over heade expect(warnSpy).toHaveBeenCalledWith(testData.PROCESS_DROPDOWN_HEADER_SLOT_TAKES_PRIORITY_WARN_MSG); }); +test('processNode does not log warning when lazy pic has width or height', + () => { + const nodeProcessor = getNewDefaultNodeProcessor(); + + const testCode = ''; + const testNode = parseHTML(testCode)[0] as MbNode; + + const consoleSpy = jest.spyOn(logger, 'warn'); + + nodeProcessor.processNode(testNode, new Context(path.resolve(''), [], {}, {})); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + +test('processNode does not log warning when lazy annotate has width or height', + () => { + const nodeProcessor = getNewDefaultNodeProcessor(); + + const testCode = ''; + const testNode = parseHTML(testCode)[0] as MbNode; + + const consoleSpy = jest.spyOn(logger, 'warn'); + + nodeProcessor.processNode(testNode, new Context(path.resolve(''), [], {}, {})); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + +test('processNode logs warning when lazy pic no width and height', + () => { + const nodeProcessor = getNewDefaultNodeProcessor(); + + const testCode = ''; + const testNode = parseHTML(testCode)[0] as MbNode; + + const consoleSpy = jest.spyOn(logger, 'warn'); + + nodeProcessor.processNode(testNode, new Context('testpath.md', [], {}, {})); + + expect(consoleSpy).toHaveBeenCalledWith('testpath.md --- ' + + 'Both width and height are not specified when using lazy loading in the file and' + + ' it might cause shifting in page layouts. ' + + 'To ensure proper functioning of lazy loading, please specify either one or both.\n'); + }); + +test('processNode logs warning when lazy annotate no width and height', + () => { + const nodeProcessor = getNewDefaultNodeProcessor(); + + const testCode = ''; + const testNode = parseHTML(testCode)[0] as MbNode; + + const consoleSpy = jest.spyOn(logger, 'warn'); + + nodeProcessor.processNode(testNode, new Context('testpath.md', [], {}, {})); + + expect(consoleSpy).toHaveBeenCalledWith('testpath.md --- ' + + 'Both width and height are not specified when using lazy loading in the file and' + + ' it might cause shifting in page layouts. ' + + 'To ensure proper functioning of lazy loading, please specify either one or both.\n'); + }); + test('markdown coverts inline colour syntax correctly', async () => { const nodeProcessor = getNewDefaultNodeProcessor(); const indexPath = 'index.md'; diff --git a/packages/vue-components/src/Pic.vue b/packages/vue-components/src/Pic.vue index 2d17bba5a4..5218410180 100644 --- a/packages/vue-components/src/Pic.vue +++ b/packages/vue-components/src/Pic.vue @@ -5,8 +5,10 @@ :src="src" :alt="alt" :width="computedWidth" + :height="computedHeight" + :loading="computedLoadType" class="img-fluid rounded" - @load.once="computeWidth" + @load.once="computeWidthAndHeight" /> @@ -35,6 +37,10 @@ export default { type: String, default: '', }, + lazy: { + type: Boolean, + default: false, + }, addClass: { type: String, default: '', @@ -53,20 +59,30 @@ export default { } return this.widthFromHeight; }, + computedHeight() { + return this.heightFromWidth; + }, + computedLoadType() { + return this.lazy ? 'lazy' : 'eager'; + }, }, data() { return { widthFromHeight: '', + heightFromWidth: '', }; }, methods: { - computeWidth() { - if (!this.hasWidth && this.hasHeight) { - const renderedImg = this.$refs.pic; - const imgHeight = renderedImg.naturalHeight; - const imgWidth = renderedImg.naturalWidth; - const aspectRatio = imgWidth / imgHeight; + computeWidthAndHeight() { + const renderedImg = this.$refs.pic; + const imgHeight = renderedImg.naturalHeight; + const imgWidth = renderedImg.naturalWidth; + const aspectRatio = imgWidth / imgHeight; + if (this.hasWidth) { // if width is present, overwrite the height (if any) to maintain aspect ratio + this.heightFromWidth = Math.round(toNumber(this.width) / aspectRatio).toString(); + } else if (this.hasHeight) { this.widthFromHeight = Math.round(toNumber(this.height) * aspectRatio).toString(); + this.heightFromWidth = this.height; } }, }, diff --git a/packages/vue-components/src/__tests__/__snapshots__/Annotation.spec.js.snap b/packages/vue-components/src/__tests__/__snapshots__/Annotation.spec.js.snap index 318783c4b1..8b8bfe2693 100644 --- a/packages/vue-components/src/__tests__/__snapshots__/Annotation.spec.js.snap +++ b/packages/vue-components/src/__tests__/__snapshots__/Annotation.spec.js.snap @@ -6,6 +6,8 @@ exports[`Annotation with customised annotation points 1`] = ` > @@ -92,6 +94,8 @@ exports[`Annotation with different visual annotation points 1`] = ` > @@ -380,6 +384,8 @@ exports[`Annotation with markdown in header, content and label 1`] = ` > diff --git a/packages/vue-components/src/annotations/Annotate.vue b/packages/vue-components/src/annotations/Annotate.vue index 62235637c6..528b560d3c 100644 --- a/packages/vue-components/src/annotations/Annotate.vue +++ b/packages/vue-components/src/annotations/Annotate.vue @@ -5,8 +5,10 @@ :src="src" :alt="alt" :width="computedWidth" + :height="computedHeight" + :loading="computedLoadType" class="annotate-image" - @load.once="getWidth" + @load.once="computeWidthAndHeight" />
@@ -35,6 +37,10 @@ export default { type: String, default: '', }, + lazy: { + type: Boolean, + default: false, + }, addClass: { type: String, default: '', @@ -53,20 +59,30 @@ export default { } return this.widthFromHeight; }, + computedHeight() { + return this.heightFromWidth; + }, + computedLoadType() { + return this.lazy ? 'lazy' : 'eager'; + }, }, data() { return { widthFromHeight: '', + heightFromWidth: '', }; }, methods: { - getWidth() { - if (!this.hasWidth && this.hasHeight) { - const renderedImg = this.$refs.pic; - const imgHeight = renderedImg.naturalHeight; - const imgWidth = renderedImg.naturalWidth; - const imageAspectRatio = imgWidth / imgHeight; - this.widthFromHeight = Math.round(toNumber(this.height) * imageAspectRatio); + computeWidthAndHeight() { + const renderedImg = this.$refs.pic; + const imgHeight = renderedImg.naturalHeight; + const imgWidth = renderedImg.naturalWidth; + const aspectRatio = imgWidth / imgHeight; + if (this.hasWidth) { // if width is present, overwrite the height (if any) to maintain aspect ratio + this.heightFromWidth = Math.round(toNumber(this.width) / aspectRatio).toString(); + } else if (this.hasHeight) { + this.widthFromHeight = Math.round(toNumber(this.height) * aspectRatio).toString(); + this.heightFromWidth = this.height; } }, },