diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..6b49d08
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ extends: [
+ '@nextcloud/eslint-config',
+ ],
+ globals: {
+ OCA: 'readonly',
+ OC: 'readonly',
+ },
+ overrides: [
+ {
+ files: ['src/__tests__/**/*'],
+ rules: {
+ 'n/no-unpublished-import': 'off',
+ 'jsdoc/require-jsdoc': 'off',
+ },
+ },
+ ],
+}
diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml
new file mode 100644
index 0000000..af11871
--- /dev/null
+++ b/.github/workflows/lint-eslint.yml
@@ -0,0 +1,100 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+#
+# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: MIT
+
+name: Lint eslint
+
+on: pull_request
+
+permissions:
+ contents: read
+
+concurrency:
+ group: lint-eslint-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ changes:
+ runs-on: ubuntu-latest-low
+ permissions:
+ contents: read
+ pull-requests: read
+
+ outputs:
+ src: ${{ steps.changes.outputs.src}}
+
+ steps:
+ - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
+ id: changes
+ continue-on-error: true
+ with:
+ filters: |
+ src:
+ - '.github/workflows/**'
+ - 'src/**'
+ - 'appinfo/info.xml'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'tsconfig.json'
+ - '.eslintrc.*'
+ - '.eslintignore'
+ - '**.js'
+ - '**.ts'
+ - '**.vue'
+
+ lint:
+ runs-on: ubuntu-latest
+
+ needs: changes
+ if: needs.changes.outputs.src != 'false'
+
+ name: NPM lint
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Read package.json node and npm engines version
+ uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
+ id: versions
+ with:
+ fallbackNode: '^24'
+ fallbackNpm: '^11.3'
+
+ - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version: ${{ steps.versions.outputs.nodeVersion }}
+
+ - name: Set up npm ${{ steps.versions.outputs.npmVersion }}
+ run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
+
+ - name: Install dependencies
+ env:
+ CYPRESS_INSTALL_BINARY: 0
+ PUPPETEER_SKIP_DOWNLOAD: true
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ summary:
+ permissions:
+ contents: none
+ runs-on: ubuntu-latest-low
+ needs: [changes, lint]
+
+ if: always()
+
+ # This is the summary, we just avoid to rename it so that branch protection rules still match
+ name: eslint
+
+ steps:
+ - name: Summary status
+ run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi
diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml
new file mode 100644
index 0000000..336cdd7
--- /dev/null
+++ b/.github/workflows/node-test.yml
@@ -0,0 +1,104 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+#
+# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: MIT
+
+name: Node tests
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ - master
+ - stable*
+
+permissions:
+ contents: read
+
+concurrency:
+ group: node-tests-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ changes:
+ runs-on: ubuntu-latest-low
+ permissions:
+ contents: read
+ pull-requests: read
+
+ outputs:
+ src: ${{ steps.changes.outputs.src}}
+
+ steps:
+ - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
+ id: changes
+ continue-on-error: true
+ with:
+ filters: |
+ src:
+ - '.github/workflows/**'
+ - 'src/**'
+ - 'appinfo/info.xml'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'tsconfig.json'
+ - 'vitest.config.*'
+ - '**.js'
+ - '**.ts'
+ - '**.vue'
+
+ test:
+ runs-on: ubuntu-latest
+
+ needs: changes
+ if: needs.changes.outputs.src != 'false'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+
+ - name: Read package.json node and npm engines version
+ uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
+ id: versions
+ with:
+ fallbackNode: '^24'
+ fallbackNpm: '^11.3'
+
+ - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version: ${{ steps.versions.outputs.nodeVersion }}
+
+ - name: Set up npm ${{ steps.versions.outputs.npmVersion }}
+ run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
+
+ - name: Install dependencies & build
+ env:
+ CYPRESS_INSTALL_BINARY: 0
+ PUPPETEER_SKIP_DOWNLOAD: true
+ run: |
+ npm ci
+ npm run build --if-present
+
+ - name: Test
+ run: npm run test
+
+ summary:
+ permissions:
+ contents: none
+ runs-on: ubuntu-latest-low
+ needs: [changes, test]
+
+ if: always()
+
+ name: npm-test-summary
+
+ steps:
+ - name: Summary status
+ run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi
diff --git a/package.json b/package.json
index 9a1a38c..c6bb488 100644
--- a/package.json
+++ b/package.json
@@ -24,11 +24,13 @@
"build": "vite --mode production build",
"dev": "vite --mode development build",
"watch": "vite --mode development build --watch",
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ext .js,.vue,.ts src",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@mdi/svg": "^7.3.67",
+ "@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "^2.4.0",
"@nextcloud/dialogs": "^5.0.0",
"@nextcloud/event-bus": "^3.1.0",
@@ -37,6 +39,8 @@
"@nextcloud/l10n": "^3.0.0",
"@nextcloud/logger": "^3.0.0",
"@nextcloud/paths": "^3.0.0",
+ "@nextcloud/router": "^3.1.0",
+ "@nextcloud/sharing": "^0.3.0",
"jszip": "^3.10.1"
},
"devDependencies": {
diff --git a/src/__tests__/MindMap.spec.js b/src/__tests__/MindMap.spec.js
new file mode 100644
index 0000000..0cae0c0
--- /dev/null
+++ b/src/__tests__/MindMap.spec.js
@@ -0,0 +1,111 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { shallowMount } from '@vue/test-utils'
+import MindMap from '../views/MindMap.vue'
+
+vi.mock('@nextcloud/l10n', () => ({ getLanguage: () => 'en' }))
+vi.mock('@nextcloud/router', () => ({
+ generateUrl: (path, params = {}) =>
+ Object.entries(params).reduce(
+ (acc, [k, v]) => acc.replace(`{${k}}`, v ?? ''),
+ path,
+ ),
+}))
+vi.mock('@nextcloud/sharing/public', () => ({
+ isPublicShare: vi.fn(() => false),
+ getSharingToken: () => '',
+}))
+
+// Viewer mixin that supplies the props/methods the component expects
+const viewerMixin = {
+ data() {
+ return {
+ fileList: [],
+ fileid: null,
+ source: null,
+ davPath: null,
+ }
+ },
+ methods: {
+ doneLoading() {},
+ handleWebviewerloaded() {},
+ },
+}
+
+function mountMindMap(dataOverrides = {}) {
+ return shallowMount(MindMap, {
+ mixins: [
+ {
+ ...viewerMixin,
+ data() {
+ return { ...viewerMixin.data(), ...dataOverrides }
+ },
+ },
+ ],
+ })
+}
+
+describe('MindMap.vue', () => {
+ beforeEach(() => {
+ window.OCA = { FilesMindMap: { setFile: vi.fn() } }
+ })
+
+ afterEach(() => {
+ delete window.OCA
+ })
+
+ it('renders an iframe element', () => {
+ const wrapper = mountMindMap()
+ expect(wrapper.find('iframe').exists()).toBe(true)
+ })
+
+ describe('iframeSrc computed property', () => {
+ it('uses source when available', () => {
+ const wrapper = mountMindMap({
+ source: '/remote.php/dav/files/user/test.km',
+ })
+ expect(wrapper.vm.iframeSrc).toContain('/remote.php/dav/files/user/test.km')
+ })
+
+ it('falls back to davPath when source is null', () => {
+ const wrapper = mountMindMap({
+ source: null,
+ davPath: '/dav/path/test.km',
+ })
+ expect(wrapper.vm.iframeSrc).toContain('/dav/path/test.km')
+ })
+ })
+
+ describe('file computed property', () => {
+ it('returns the file whose fileid matches the current fileid', () => {
+ const file = { fileid: 7, name: 'test.km' }
+ const wrapper = mountMindMap({ fileList: [file], fileid: 7 })
+ expect(wrapper.vm.file).toBe(file)
+ })
+
+ it('returns undefined when no file matches', () => {
+ const wrapper = mountMindMap({
+ fileList: [{ fileid: 1, name: 'other.km' }],
+ fileid: 99,
+ })
+ expect(wrapper.vm.file).toBeUndefined()
+ })
+ })
+
+ describe('lifecycle hooks', () => {
+ it('calls OCA.FilesMindMap.setFile on mount', () => {
+ mountMindMap({
+ fileList: [{ fileid: 3, name: 'test.km' }],
+ fileid: 3,
+ })
+ expect(window.OCA.FilesMindMap.setFile).toHaveBeenCalled()
+ })
+
+ it('removes the webviewerloaded event listener on destroy', () => {
+ const spy = vi.spyOn(document, 'removeEventListener')
+ const wrapper = mountMindMap()
+ wrapper.destroy()
+ expect(spy).toHaveBeenCalledWith('webviewerloaded', expect.anything())
+ spy.mockRestore()
+ })
+ })
+})
diff --git a/src/__tests__/mindmap.spec.js b/src/__tests__/mindmap.spec.js
new file mode 100644
index 0000000..af88b3e
--- /dev/null
+++ b/src/__tests__/mindmap.spec.js
@@ -0,0 +1,448 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+import FilesMindMap from '../mindmap.js'
+import { showMessage as showToast } from '@nextcloud/dialogs'
+import axios from '@nextcloud/axios'
+
+const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0))
+
+vi.mock('@nextcloud/l10n', () => ({
+ translate: (_app, text) => text,
+}))
+
+vi.mock('@nextcloud/router', () => ({
+ generateUrl: (path) => `/nc${path}`,
+}))
+
+vi.mock('@nextcloud/dialogs', () => ({
+ showMessage: vi.fn(() => ({ hideToast: vi.fn() })),
+}))
+
+vi.mock('@nextcloud/auth', () => ({
+ getCurrentUser: vi.fn(() => ({ uid: 'testuser' })),
+}))
+
+vi.mock('@nextcloud/sharing/public', () => ({
+ isPublicShare: vi.fn(() => false),
+}))
+
+vi.mock('@nextcloud/event-bus', () => ({
+ emit: vi.fn(),
+}))
+
+vi.mock('@mdi/svg/svg/pencil.svg?raw', () => ({ default: '' }))
+
+vi.mock('@nextcloud/files-legacy', () => ({
+ FileAction: vi.fn().mockImplementation(opts => opts),
+ registerFileAction: vi.fn(),
+ addNewFileMenuEntry: vi.fn(),
+}))
+
+vi.mock('@nextcloud/files', () => ({
+ DefaultType: { HIDDEN: 'hidden' },
+ FileAction: vi.fn().mockImplementation(opts => opts),
+ addNewFileMenuEntry: vi.fn(),
+ registerFileAction: vi.fn(),
+ File: vi.fn(),
+ Permission: { READ: 1, CREATE: 4, UPDATE: 2, DELETE: 8, SHARE: 16, ALL: 31 },
+ getUniqueName: vi.fn(name => name),
+}))
+
+vi.mock('@nextcloud/axios', () => {
+ const fn = vi.fn()
+ fn.get = vi.fn()
+ return { default: fn }
+})
+
+vi.mock('../plugins/km', () => ({
+ default: { name: 'km', mimes: ['application/km'], encode: vi.fn(d => Promise.resolve(d)), decode: vi.fn() },
+}))
+vi.mock('../plugins/freemind', () => ({
+ default: { name: 'freemind', mimes: ['application/x-freemind'], encode: null, decode: vi.fn() },
+}))
+vi.mock('../plugins/xmind', () => ({
+ default: { name: 'xmind', mimes: ['application/vnd.xmind.workbook'], encode: null, decode: vi.fn() },
+}))
+
+describe('FilesMindMap', () => {
+ beforeEach(() => {
+ FilesMindMap._extensions = []
+ FilesMindMap._file = {}
+ FilesMindMap._currentContext = null
+ vi.clearAllMocks()
+ })
+
+ // ─── Extension management ───────────────────────────────────────────────────
+
+ describe('registerExtension', () => {
+ it('registers a single extension object', () => {
+ const ext = { name: 'test', mimes: ['application/test'] }
+ FilesMindMap.registerExtension(ext)
+ expect(FilesMindMap._extensions).toHaveLength(1)
+ expect(FilesMindMap._extensions[0]).toBe(ext)
+ })
+
+ it('registers an array of extensions', () => {
+ const ext1 = { name: 'a', mimes: ['application/a'] }
+ const ext2 = { name: 'b', mimes: ['application/b'] }
+ FilesMindMap.registerExtension([ext1, ext2])
+ expect(FilesMindMap._extensions).toHaveLength(2)
+ })
+ })
+
+ describe('getExtensionByMime', () => {
+ it('returns the matching extension for a registered mime type', () => {
+ const ext = { name: 'km', mimes: ['application/km'] }
+ FilesMindMap.registerExtension(ext)
+ expect(FilesMindMap.getExtensionByMime('application/km')).toBe(ext)
+ })
+
+ it('returns null for an unknown mime type', () => {
+ expect(FilesMindMap.getExtensionByMime('application/unknown')).toBeNull()
+ })
+
+ it('returns null when no extensions are registered', () => {
+ expect(FilesMindMap.getExtensionByMime('application/km')).toBeNull()
+ })
+ })
+
+ describe('isSupportedMime', () => {
+ it('returns true for a registered mime type', () => {
+ FilesMindMap.registerExtension({ name: 'km', mimes: ['application/km'] })
+ expect(FilesMindMap.isSupportedMime('application/km')).toBe(true)
+ })
+
+ it('returns false for an unregistered mime type', () => {
+ expect(FilesMindMap.isSupportedMime('application/pdf')).toBe(false)
+ })
+ })
+
+ describe('getSupportedMimetypes', () => {
+ it('returns a flat list of all mimes from all registered extensions', () => {
+ FilesMindMap.registerExtension([
+ { name: 'a', mimes: ['application/a', 'application/a2'] },
+ { name: 'b', mimes: ['application/b'] },
+ ])
+ expect(FilesMindMap.getSupportedMimetypes()).toEqual([
+ 'application/a',
+ 'application/a2',
+ 'application/b',
+ ])
+ })
+
+ it('returns an empty array when no extensions are registered', () => {
+ expect(FilesMindMap.getSupportedMimetypes()).toEqual([])
+ })
+ })
+
+ // ─── Notifications ─────────────────────────────────────────────────────────
+
+ describe('showMessage', () => {
+ it('calls showToast with the message and the default 3 s timeout', () => {
+ FilesMindMap.showMessage('Hello')
+ expect(showToast).toHaveBeenCalledWith('Hello', { timeout: 3000 })
+ })
+
+ it('calls showToast with a custom timeout', () => {
+ FilesMindMap.showMessage('Hello', 5000)
+ expect(showToast).toHaveBeenCalledWith('Hello', { timeout: 5000 })
+ })
+
+ it('returns the toast object from showToast', () => {
+ const mockToast = { hideToast: vi.fn() }
+ showToast.mockReturnValue(mockToast)
+ expect(FilesMindMap.showMessage('Hello')).toBe(mockToast)
+ })
+ })
+
+ describe('hideMessage', () => {
+ it('calls hideToast on the toast object', () => {
+ const mockToast = { hideToast: vi.fn() }
+ FilesMindMap.hideMessage(mockToast)
+ expect(mockToast.hideToast).toHaveBeenCalledOnce()
+ })
+
+ it('does not throw when called with null', () => {
+ expect(() => FilesMindMap.hideMessage(null)).not.toThrow()
+ })
+
+ it('does not throw when called with undefined', () => {
+ expect(() => FilesMindMap.hideMessage(undefined)).not.toThrow()
+ })
+
+ it('does not throw when the toast has no hideToast method', () => {
+ expect(() => FilesMindMap.hideMessage({})).not.toThrow()
+ })
+ })
+
+ // ─── File state ────────────────────────────────────────────────────────────
+
+ describe('setFile', () => {
+ it('sets name, dir, and fullName from file object', () => {
+ FilesMindMap.setFile({ filename: '/documents/test.km', basename: 'test.km' })
+ expect(FilesMindMap._file.name).toBe('test.km')
+ expect(FilesMindMap._file.dir).toBe('/documents')
+ expect(FilesMindMap._file.fullName).toBe('/documents/test.km')
+ })
+
+ it('sets dir to "/" for top-level files', () => {
+ FilesMindMap.setFile({ filename: '/test.km', basename: 'test.km' })
+ expect(FilesMindMap._file.dir).toBe('/')
+ })
+
+ it('sets _currentContext from the resolved dir', () => {
+ FilesMindMap.setFile({ filename: '/docs/sub/test.km', basename: 'test.km' })
+ expect(FilesMindMap._currentContext).toEqual(expect.objectContaining({ dir: '/docs/sub' }))
+ })
+ })
+
+ // ─── Public share detection ────────────────────────────────────────────────
+
+ describe('isMindmapPublic', () => {
+ it('returns false when not on a public share page', async () => {
+ const { isPublicShare } = await import('@nextcloud/sharing/public')
+ isPublicShare.mockReturnValue(false)
+ expect(FilesMindMap.isMindmapPublic()).toBe(false)
+ })
+
+ it('returns true when on a public share page with a supported mime type', async () => {
+ const { isPublicShare } = await import('@nextcloud/sharing/public')
+ isPublicShare.mockReturnValue(true)
+ FilesMindMap.registerExtension({ name: 'km', mimes: ['application/km'] })
+
+ const input = document.createElement('input')
+ input.id = 'mimetype'
+ input.value = 'application/km'
+ document.body.appendChild(input)
+ try {
+ expect(FilesMindMap.isMindmapPublic()).toBe(true)
+ } finally {
+ document.body.removeChild(input)
+ }
+ })
+
+ it('returns false when on a public share page but mime type is unsupported', async () => {
+ const { isPublicShare } = await import('@nextcloud/sharing/public')
+ isPublicShare.mockReturnValue(true)
+
+ const input = document.createElement('input')
+ input.id = 'mimetype'
+ input.value = 'application/pdf'
+ document.body.appendChild(input)
+ try {
+ expect(FilesMindMap.isMindmapPublic()).toBe(false)
+ } finally {
+ document.body.removeChild(input)
+ }
+ })
+ })
+
+ // ─── save() ────────────────────────────────────────────────────────────────
+
+ describe('save', () => {
+ it('calls fail immediately when the extension does not support encoding', () => {
+ FilesMindMap.registerExtension({
+ name: 'freemind',
+ mimes: ['application/x-freemind'],
+ encode: null,
+ decode: null,
+ })
+ FilesMindMap._file = { dir: '/docs', name: 'test.mm', mime: 'application/x-freemind' }
+
+ const fail = vi.fn()
+ FilesMindMap.save('data', vi.fn(), fail)
+ expect(fail).toHaveBeenCalledWith(expect.stringContaining('Does not support saving'))
+ })
+
+ it('sends a PUT request to the savefile URL on the happy path', async () => {
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn().mockResolvedValue('encoded-content'),
+ decode: null,
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km', mime: 'application/km', mtime: 100 }
+ axios.mockResolvedValue({ data: { mtime: 200 } })
+
+ const success = vi.fn()
+ FilesMindMap.save('input-data', success, vi.fn())
+ await flushPromises()
+
+ expect(axios).toHaveBeenCalledWith(expect.objectContaining({
+ method: 'PUT',
+ url: '/nc/apps/files_mindmap/ajax/savefile',
+ }))
+ expect(success).toHaveBeenCalledWith('File Saved')
+ })
+
+ it('updates _file.mtime from the server response', async () => {
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn().mockResolvedValue('data'),
+ decode: null,
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km', mime: 'application/km', mtime: 100 }
+ axios.mockResolvedValue({ data: { mtime: 999 } })
+
+ FilesMindMap.save('data', vi.fn(), vi.fn())
+ await flushPromises()
+
+ expect(FilesMindMap._file.mtime).toBe(999)
+ })
+
+ it('calls fail with the server error message on failure', async () => {
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn().mockResolvedValue('data'),
+ decode: null,
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km', mime: 'application/km', mtime: 100 }
+ axios.mockRejectedValue({ response: { data: { message: 'Quota exceeded' } } })
+
+ const fail = vi.fn()
+ FilesMindMap.save('data', vi.fn(), fail)
+ await flushPromises()
+
+ expect(fail).toHaveBeenCalledWith('Quota exceeded')
+ })
+
+ it('calls fail with generic message when error response has no message', async () => {
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn().mockResolvedValue('data'),
+ decode: null,
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km', mime: 'application/km', mtime: 100 }
+ axios.mockRejectedValue(new Error('Network error'))
+
+ const fail = vi.fn()
+ FilesMindMap.save('data', vi.fn(), fail)
+ await flushPromises()
+
+ expect(fail).toHaveBeenCalledWith('Save failed')
+ })
+ })
+
+ // ─── load() ────────────────────────────────────────────────────────────────
+
+ describe('load', () => {
+ it('calls success with decoded file contents', async () => {
+ const decoded = { root: { data: { text: 'Test' } } }
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn(),
+ decode: vi.fn().mockResolvedValue(decoded),
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km' }
+
+ axios.get.mockResolvedValue({
+ data: {
+ filecontents: btoa('raw-content'),
+ mime: 'application/km',
+ writeable: true,
+ mtime: 9999,
+ },
+ })
+
+ const success = vi.fn()
+ FilesMindMap.load(success, vi.fn())
+ await flushPromises()
+
+ expect(success).toHaveBeenCalledWith(JSON.stringify(decoded))
+ })
+
+ it('sets _file.mime and _file.mtime from the server response', async () => {
+ const ext = {
+ name: 'km',
+ mimes: ['application/km'],
+ encode: vi.fn(),
+ decode: vi.fn().mockResolvedValue({}),
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.km' }
+
+ axios.get.mockResolvedValue({
+ data: {
+ filecontents: btoa('content'),
+ mime: 'application/km',
+ writeable: true,
+ mtime: 42,
+ },
+ })
+
+ FilesMindMap.load(vi.fn(), vi.fn())
+ await flushPromises()
+
+ expect(FilesMindMap._file.mime).toBe('application/km')
+ expect(FilesMindMap._file.mtime).toBe(42)
+ })
+
+ it('calls failure when the HTTP request fails', async () => {
+ FilesMindMap._file = { dir: '/docs', name: 'test.km' }
+ axios.get.mockRejectedValue({
+ response: { data: { message: 'File not found' } },
+ message: 'Request failed',
+ })
+
+ const failure = vi.fn()
+ FilesMindMap.load(vi.fn(), failure)
+ await flushPromises()
+
+ expect(failure).toHaveBeenCalledWith('File not found')
+ })
+
+ it('calls failure for an unsupported mime type', async () => {
+ FilesMindMap._extensions = []
+ FilesMindMap._file = { dir: '/docs', name: 'test.pdf' }
+
+ axios.get.mockResolvedValue({
+ data: {
+ filecontents: btoa('content'),
+ mime: 'application/pdf',
+ writeable: true,
+ mtime: 100,
+ },
+ })
+
+ const failure = vi.fn()
+ FilesMindMap.load(vi.fn(), failure)
+ await flushPromises()
+
+ expect(failure).toHaveBeenCalledWith(expect.stringContaining('Unsupported file type'))
+ })
+
+ it('marks supportedWrite as false for read-only extensions', async () => {
+ const ext = {
+ name: 'freemind',
+ mimes: ['application/x-freemind'],
+ encode: null,
+ decode: vi.fn().mockResolvedValue('decoded'),
+ }
+ FilesMindMap._extensions = [ext]
+ FilesMindMap._file = { dir: '/docs', name: 'test.mm' }
+
+ axios.get.mockResolvedValue({
+ data: {
+ filecontents: btoa('content'),
+ mime: 'application/x-freemind',
+ writeable: true,
+ mtime: 1,
+ },
+ })
+
+ FilesMindMap.load(vi.fn(), vi.fn())
+ await flushPromises()
+
+ expect(FilesMindMap._file.supportedWrite).toBe(false)
+ })
+ })
+})
diff --git a/src/__tests__/plugins/freemind.spec.js b/src/__tests__/plugins/freemind.spec.js
new file mode 100644
index 0000000..962fab5
--- /dev/null
+++ b/src/__tests__/plugins/freemind.spec.js
@@ -0,0 +1,105 @@
+import { describe, it, expect, beforeAll } from 'vitest'
+import util from '../../util.js'
+import freemind from '../../plugins/freemind.js'
+
+// freemind.toKm references the legacy global FilesMindMap.Util
+beforeAll(() => {
+ global.FilesMindMap = { Util: util }
+})
+
+describe('freemind plugin', () => {
+ it('has the correct name', () => {
+ expect(freemind.name).toBe('freemind')
+ })
+
+ it('supports application/x-freemind mime type', () => {
+ expect(freemind.mimes).toContain('application/x-freemind')
+ })
+
+ it('encode is null (read-only format)', () => {
+ expect(freemind.encode).toBeNull()
+ })
+
+ describe('markerMap', () => {
+ it('maps full-1 to priority 1', () => {
+ expect(freemind.markerMap['full-1']).toEqual(['priority', 1])
+ })
+
+ it('maps full-8 to priority 8', () => {
+ expect(freemind.markerMap['full-8']).toEqual(['priority', 8])
+ })
+ })
+
+ describe('processTopic', () => {
+ it('extracts TEXT as data.text', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Hello' }, obj)
+ expect(obj.data.text).toBe('Hello')
+ })
+
+ it('extracts hyperlink from LINK attribute', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Link', LINK: 'https://example.com' }, obj)
+ expect(obj.data.hyperlink).toBe('https://example.com')
+ })
+
+ it('extracts a single priority marker', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Prio', icon: { BUILTIN: 'full-3' } }, obj)
+ expect(obj.data.priority).toBe(3)
+ })
+
+ it('extracts multiple markers from an array', () => {
+ const obj = {}
+ freemind.processTopic({
+ TEXT: 'Multi',
+ icon: [{ BUILTIN: 'full-1' }, { BUILTIN: 'full-2' }],
+ }, obj)
+ expect(obj.data.priority).toBe(2) // last one wins
+ })
+
+ it('ignores unknown marker values', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Unknown', icon: { BUILTIN: 'unknown-marker' } }, obj)
+ expect(obj.data.priority).toBeUndefined()
+ })
+
+ it('processes a single child node', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Parent', node: { TEXT: 'Child' } }, obj)
+ expect(obj.children).toHaveLength(1)
+ expect(obj.children[0].data.text).toBe('Child')
+ })
+
+ it('processes multiple child nodes', () => {
+ const obj = {}
+ freemind.processTopic({
+ TEXT: 'Parent',
+ node: [{ TEXT: 'Child1' }, { TEXT: 'Child2' }],
+ }, obj)
+ expect(obj.children).toHaveLength(2)
+ expect(obj.children[0].data.text).toBe('Child1')
+ expect(obj.children[1].data.text).toBe('Child2')
+ })
+
+ it('produces no children property when topic has no children', () => {
+ const obj = {}
+ freemind.processTopic({ TEXT: 'Leaf' }, obj)
+ expect(obj.children).toBeUndefined()
+ })
+ })
+
+ describe('decode', () => {
+ it('decodes a FreeMind XML string into a km-compatible tree', async () => {
+ const xml = ''
+ const result = await freemind.decode(xml)
+ expect(result.data.text).toBe('Root')
+ expect(result.children).toHaveLength(1)
+ expect(result.children[0].data.text).toBe('Child')
+ })
+
+ it('rejects on malformed input', async () => {
+ await expect(freemind.decode(null)).rejects.toBeDefined()
+ })
+ })
+})
diff --git a/src/__tests__/plugins/km.spec.js b/src/__tests__/plugins/km.spec.js
new file mode 100644
index 0000000..1390ed5
--- /dev/null
+++ b/src/__tests__/plugins/km.spec.js
@@ -0,0 +1,40 @@
+import { describe, it, expect } from 'vitest'
+import km from '../../plugins/km.js'
+
+describe('km plugin', () => {
+ it('has the correct name', () => {
+ expect(km.name).toBe('km')
+ })
+
+ it('supports application/km mime type', () => {
+ expect(km.mimes).toContain('application/km')
+ })
+
+ describe('encode', () => {
+ it('resolves with the same data unchanged', async () => {
+ const data = '{"root":{"data":{"text":"Test"}}}'
+ await expect(km.encode(data)).resolves.toBe(data)
+ })
+
+ it('resolves with empty string', async () => {
+ await expect(km.encode('')).resolves.toBe('')
+ })
+ })
+
+ describe('decode', () => {
+ it('parses a valid JSON string and resolves with the object', async () => {
+ const obj = { root: { data: { text: 'Test' } } }
+ const result = await km.decode(JSON.stringify(obj))
+ expect(result).toEqual(obj)
+ })
+
+ it('resolves with the raw string when JSON is invalid', async () => {
+ const data = 'not-valid-json'
+ await expect(km.decode(data)).resolves.toBe(data)
+ })
+
+ it('resolves with empty string', async () => {
+ await expect(km.decode('')).resolves.toBe('')
+ })
+ })
+})
diff --git a/src/__tests__/plugins/xmind.spec.js b/src/__tests__/plugins/xmind.spec.js
new file mode 100644
index 0000000..2a0059c
--- /dev/null
+++ b/src/__tests__/plugins/xmind.spec.js
@@ -0,0 +1,108 @@
+import { describe, it, expect } from 'vitest'
+import xmind from '../../plugins/xmind.js'
+
+describe('xmind plugin', () => {
+ it('has the correct name', () => {
+ expect(xmind.name).toBe('xmind')
+ })
+
+ it('supports application/vnd.xmind.workbook mime type', () => {
+ expect(xmind.mimes).toContain('application/vnd.xmind.workbook')
+ })
+
+ it('encode is null (read-only format)', () => {
+ expect(xmind.encode).toBeNull()
+ })
+
+ describe('markerMap', () => {
+ it('maps priority-1 to priority 1', () => {
+ expect(xmind.markerMap['priority-1']).toEqual(['priority', 1])
+ })
+
+ it('maps priority-8 to priority 8', () => {
+ expect(xmind.markerMap['priority-8']).toEqual(['priority', 8])
+ })
+
+ it('maps task-done to progress 9', () => {
+ expect(xmind.markerMap['task-done']).toEqual(['progress', 9])
+ })
+
+ it('maps task-half to progress 5', () => {
+ expect(xmind.markerMap['task-half']).toEqual(['progress', 5])
+ })
+ })
+
+ describe('processTopic', () => {
+ it('extracts title as data.text', () => {
+ const obj = {}
+ xmind.processTopic({ title: 'My Topic' }, obj)
+ expect(obj.data.text).toBe('My Topic')
+ })
+
+ it('extracts hyperlink from xlink:href', () => {
+ const obj = {}
+ xmind.processTopic({ title: 'Link', 'xlink:href': 'https://example.com' }, obj)
+ expect(obj.data.hyperlink).toBe('https://example.com')
+ })
+
+ it('extracts a single marker', () => {
+ const obj = {}
+ xmind.processTopic({
+ title: 'Prio',
+ marker_refs: { marker_ref: { marker_id: 'priority-2' } },
+ }, obj)
+ expect(obj.data.priority).toBe(2)
+ })
+
+ it('extracts multiple markers from an array', () => {
+ const obj = {}
+ xmind.processTopic({
+ title: 'Multi',
+ marker_refs: {
+ marker_ref: [
+ { marker_id: 'priority-3' },
+ { marker_id: 'task-done' },
+ ],
+ },
+ }, obj)
+ expect(obj.data.priority).toBe(3)
+ expect(obj.data.progress).toBe(9)
+ })
+
+ it('ignores unknown markers', () => {
+ const obj = {}
+ xmind.processTopic({
+ title: 'Unknown',
+ marker_refs: { marker_ref: { marker_id: 'unknown-marker' } },
+ }, obj)
+ expect(obj.data.priority).toBeUndefined()
+ })
+
+ it('processes multiple child topics', () => {
+ const obj = {}
+ xmind.processTopic({
+ title: 'Root',
+ children: {
+ topics: {
+ topic: [{ title: 'Child1' }, { title: 'Child2' }],
+ },
+ },
+ }, obj)
+ expect(obj.children).toHaveLength(2)
+ expect(obj.children[0].data.text).toBe('Child1')
+ expect(obj.children[1].data.text).toBe('Child2')
+ })
+
+ it('produces no children property for leaf topics', () => {
+ const obj = {}
+ xmind.processTopic({ title: 'Leaf' }, obj)
+ expect(obj.children).toBeUndefined()
+ })
+ })
+
+ describe('readDocument', () => {
+ it('rejects when content.xml is missing in the zip', async () => {
+ await expect(xmind.readDocument('not a zip')).rejects.toBeDefined()
+ })
+ })
+})
diff --git a/src/__tests__/util.spec.js b/src/__tests__/util.spec.js
new file mode 100644
index 0000000..e8cff23
--- /dev/null
+++ b/src/__tests__/util.spec.js
@@ -0,0 +1,89 @@
+import { describe, it, expect } from 'vitest'
+import util from '../util.js'
+
+describe('util.jsVar', () => {
+ it('replaces hyphens with underscores', () => {
+ expect(util.jsVar('foo-bar-baz')).toBe('foo_bar_baz')
+ })
+
+ it('returns empty string for null', () => {
+ expect(util.jsVar(null)).toBe('')
+ })
+
+ it('returns empty string for undefined', () => {
+ expect(util.jsVar(undefined)).toBe('')
+ })
+
+ it('leaves strings without hyphens unchanged', () => {
+ expect(util.jsVar('foobar')).toBe('foobar')
+ })
+})
+
+describe('util.toArray', () => {
+ it('wraps a non-array value in an array', () => {
+ expect(util.toArray('foo')).toEqual(['foo'])
+ expect(util.toArray(42)).toEqual([42])
+ })
+
+ it('wraps an object in an array', () => {
+ const obj = { a: 1 }
+ expect(util.toArray(obj)).toEqual([obj])
+ })
+
+ it('returns arrays unchanged', () => {
+ expect(util.toArray([1, 2, 3])).toEqual([1, 2, 3])
+ })
+
+ it('returns empty arrays unchanged', () => {
+ expect(util.toArray([])).toEqual([])
+ })
+})
+
+describe('util.base64Encode / util.base64Decode', () => {
+ it('round-trips ASCII text', () => {
+ const text = 'Hello, World!'
+ expect(util.base64Decode(util.base64Encode(text))).toBe(text)
+ })
+
+ it('round-trips unicode text', () => {
+ const text = 'Héllo Wörld — こんにちは'
+ expect(util.base64Decode(util.base64Encode(text))).toBe(text)
+ })
+
+ it('base64Encode returns a non-empty string', () => {
+ expect(util.base64Encode('test')).toBeTruthy()
+ })
+
+ it('base64Decode handles plain btoa-encoded ASCII', () => {
+ expect(util.base64Decode(btoa('hello'))).toBe('hello')
+ })
+})
+
+describe('util.xml2json', () => {
+ it('parses element attributes', () => {
+ const xml = ''
+ const result = util.xml2json(xml)
+ expect(result.id).toBe('42')
+ expect(result.name).toBe('test')
+ })
+
+ it('parses nested child elements', () => {
+ const xml = ''
+ const result = util.xml2json(xml)
+ expect(result.child).toBeDefined()
+ expect(result.child.TEXT).toBe('hello')
+ })
+
+ it('parses multiple children of the same tag as an array', () => {
+ const xml = ''
+ const result = util.xml2json(xml)
+ expect(Array.isArray(result.node)).toBe(true)
+ expect(result.node).toHaveLength(2)
+ })
+
+ it('returns defined result for a minimal document', () => {
+ const xml = ''
+ const result = util.xml2json(xml)
+ expect(result).toBeDefined()
+ })
+})
diff --git a/src/mindmap.js b/src/mindmap.js
index d277615..2e3367c 100644
--- a/src/mindmap.js
+++ b/src/mindmap.js
@@ -1,19 +1,20 @@
-import { basename, extname } from 'path'
+/* global OCA */
+// eslint-disable-next-line import/no-unresolved
import SvgPencil from '@mdi/svg/svg/pencil.svg?raw'
+// eslint-disable-next-line import/no-unresolved
import MindMapSvg from '../img/mindmap.svg?raw'
import {
DefaultType,
- addNewFileMenuEntry,
registerFileAction,
File,
Permission,
- getUniqueName
+ getUniqueName,
} from '@nextcloud/files'
import {
FileAction,
registerFileAction as legacyRegisterFileAction,
- addNewFileMenuEntry as legacyAddNewFileMenuEntry
+ addNewFileMenuEntry as legacyAddNewFileMenuEntry,
} from '@nextcloud/files-legacy'
import { emit } from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
@@ -24,100 +25,99 @@ import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { showMessage as showToast } from '@nextcloud/dialogs'
-
-import util from './util'
-import km from './plugins/km'
-import freemind from './plugins/freemind'
-import xmind from './plugins/xmind'
+import util from './util.js'
+import km from './plugins/km.js'
+import freemind from './plugins/freemind.js'
+import xmind from './plugins/xmind.js'
const version = Number.parseInt((window.OC?.config?.version ?? '0').split('.')[0])
-var FilesMindMap = {
+const FilesMindMap = {
_currentContext: null,
_file: {},
_lastTitle: '',
_extensions: [],
- init: function() {
- this.registerExtension([km, freemind, xmind]);
+ init() {
+ this.registerExtension([km, freemind, xmind])
},
- registerExtension: function(objs) {
- var self = this;
+ registerExtension(objs) {
+ const self = this
if (!Array.isArray(objs)) {
- objs = [objs];
+ objs = [objs]
}
- objs.forEach(function(obj){
- self._extensions.push(obj);
- });
+ objs.forEach(function(obj) {
+ self._extensions.push(obj)
+ })
},
- getExtensionByMime: function(mime) {
- for (var i = 0; i < this._extensions.length; i++) {
- var obj = this._extensions[i];
+ getExtensionByMime(mime) {
+ for (let i = 0; i < this._extensions.length; i++) {
+ const obj = this._extensions[i]
if (obj.mimes.indexOf(mime) >= 0) {
- return obj;
+ return obj
}
}
- return null;
+ return null
},
- isSupportedMime: function(mime) {
- return this.getExtensionByMime(mime) !== null ? true : false;
+ isSupportedMime(mime) {
+ return this.getExtensionByMime(mime) !== null
},
- showMessage: function(msg, delay) {
- delay = delay || 3000;
- return showToast(msg, { timeout: delay });
+ showMessage(msg, delay) {
+ delay = delay || 3000
+ return showToast(msg, { timeout: delay })
},
- hideMessage: function(toast) {
+ hideMessage(toast) {
if (toast && typeof toast.hideToast === 'function') {
- toast.hideToast();
+ toast.hideToast()
}
},
/**
* Determine if this page is public mindmap share page
- * @returns {boolean}
+ * @return {boolean}
*/
- isMindmapPublic: function() {
+ isMindmapPublic() {
if (!isPublicShare()) {
- return false;
+ return false
}
- return this.isSupportedMime(document.getElementById('mimetype')?.value);
- },
+ return this.isSupportedMime(document.getElementById('mimetype')?.value)
+ },
- save: function(data, success, fail) {
- var self = this;
- var url = '';
- var path = this._file.dir + '/' + this._file.name;
+ save(data, success, fail) {
+ const self = this
+ let url = ''
+ let path = this._file.dir + '/' + this._file.name
if (this._file.dir === '/') {
- path = '/' + this._file.name;
+ path = '/' + this._file.name
}
/* 当encode方法没实现的时候无法保存 */
- var plugin = this.getExtensionByMime(this._file.mime);
+ const plugin = this.getExtensionByMime(this._file.mime)
if (plugin.encode === null) {
- fail(t('files_mindmap', 'Does not support saving {extension} files.', {extension: plugin.name}));
- return;
+ fail(t('files_mindmap', 'Does not support saving {extension} files.', { extension: plugin.name }))
+ return
}
plugin.encode(data).then(function(data2) {
- var putObject = {
+ const putObject = {
filecontents: data2,
- path: path,
- mtime: self._file.mtime // send modification time of currently loaded file
- };
+ path,
+ mtime: self._file.mtime, // send modification time of currently loaded file
+ }
if (document.getElementById('isPublic')?.value) {
- putObject.token = document.getElementById('sharingToken')?.value;
- url = generateUrl('/apps/files_mindmap/share/save');
+ putObject.token = document.getElementById('sharingToken')?.value
+ url = generateUrl('/apps/files_mindmap/share/save')
if (self.isSupportedMime(document.getElementById('mimetype')?.value)) {
- putObject.path = '';
+ putObject.path = ''
}
} else {
- url = generateUrl('/apps/files_mindmap/ajax/savefile');
+ url = generateUrl('/apps/files_mindmap/ajax/savefile')
}
axios({
@@ -127,72 +127,71 @@ var FilesMindMap = {
}).then(function(response) {
// update modification time
try {
- self._file.mtime = response.data.mtime;
- } catch(e) {}
- success(t('files_mindmap', 'File Saved'));
+ self._file.mtime = response.data.mtime
+ } catch (e) {}
+ success(t('files_mindmap', 'File Saved'))
}).catch(function(error) {
- var message = t('files_mindmap', 'Save failed');
- message = error.response.data.message;
- fail(message);
- });
- });
+ const message = error.response?.data?.message || t('files_mindmap', 'Save failed')
+ fail(message)
+ })
+ })
},
- load: function(success, failure) {
- var self = this;
- var filename = this._file.name;
- var dir = this._file.dir;
- var url = '';
- var sharingToken = '';
- var mimetype = document.getElementById('mimetype')?.value;
+ load(success, failure) {
+ const self = this
+ const filename = this._file.name
+ const dir = this._file.dir
+ let url = ''
+ let sharingToken = ''
+ const mimetype = document.getElementById('mimetype')?.value
if (document.getElementById('isPublic')?.value && this.isSupportedMime(mimetype)) {
- sharingToken = document.getElementById('sharingToken')?.value;
- url = generateUrl('/apps/files_mindmap/public/{token}', {token: sharingToken});
+ sharingToken = document.getElementById('sharingToken')?.value
+ url = generateUrl('/apps/files_mindmap/public/{token}', { token: sharingToken })
} else if (document.getElementById('isPublic')?.value) {
- sharingToken = document.getElementById('sharingToken')?.value;
+ sharingToken = document.getElementById('sharingToken')?.value
url = generateUrl('/apps/files_mindmap/public/{token}?dir={dir}&filename={filename}',
- { token: sharingToken, filename: filename, dir: dir});
+ { token: sharingToken, filename, dir })
} else {
url = generateUrl('/apps/files_mindmap/ajax/loadfile?filename={filename}&dir={dir}',
- {filename: filename, dir: dir});
+ { filename, dir })
}
axios.get(url).then(function(response) {
- var data = response.data;
- data.filecontents = util.base64Decode(data.filecontents);
- var plugin = self.getExtensionByMime(data.mime);
+ const data = response.data
+ data.filecontents = util.base64Decode(data.filecontents)
+ const plugin = self.getExtensionByMime(data.mime)
if (!plugin || plugin.decode === null) {
- failure(t('files_mindmap', 'Unsupported file type: {mimetype}', {mimetype: data.mime}));
- return;
+ failure(t('files_mindmap', 'Unsupported file type: {mimetype}', { mimetype: data.mime }))
+ return
}
- plugin.decode(data.filecontents).then(function(kmdata){
- data.filecontents = typeof kmdata === 'object' ? JSON.stringify(kmdata) : kmdata;
- data.supportedWrite = true;
+ plugin.decode(data.filecontents).then(function(kmdata) {
+ data.filecontents = typeof kmdata === 'object' ? JSON.stringify(kmdata) : kmdata
+ data.supportedWrite = true
if (plugin.encode === null) {
- data.writeable = false;
- data.supportedWrite = false;
+ data.writeable = false
+ data.supportedWrite = false
}
- self._file.writeable = data.writeable;
- self._file.supportedWrite = data.supportedWrite;
- self._file.mime = data.mime;
- self._file.mtime = data.mtime;
+ self._file.writeable = data.writeable
+ self._file.supportedWrite = data.supportedWrite
+ self._file.mime = data.mime
+ self._file.mtime = data.mtime
- success(data.filecontents);
- }, function(e){
- failure(e);
+ success(data.filecontents)
+ }, function(e) {
+ failure(e)
})
}).catch(function(error) {
- failure(error.response?.data?.message || error.message);
- });
+ failure(error.response?.data?.message || error.message)
+ })
},
/**
* @private
*/
- registerFileActions: function () {
- var mimes = this.getSupportedMimetypes(),
- _self = this;
+ registerFileActions() {
+ const mimes = this.getSupportedMimetypes()
+ const _self = this
const actionConfig = {
id: 'file_mindmap',
@@ -207,7 +206,7 @@ var FilesMindMap = {
async exec(node, view) {
try {
- OCA.Viewer.openWith('mindmap', { path: node.path });
+ OCA.Viewer.openWith('mindmap', { path: node.path })
return true
} catch (error) {
_self.showMessage(error)
@@ -225,19 +224,19 @@ var FilesMindMap = {
}
},
- registerNewFileMenuPlugin: function() {
+ registerNewFileMenuPlugin() {
legacyAddNewFileMenuEntry({
id: 'mindmapfile',
displayName: t('files_mindmap', 'New mind map file'),
...(version >= 33 ? { iconSvgInline: MindMapSvg } : { iconClass: 'icon-mindmap' }),
enabled(context) {
// only attach to main file list, public view is not supported yet
- console.log('addNewFileMenuEntry', context);
- return (context.permissions & Permission.CREATE) !== 0;
+ console.debug('addNewFileMenuEntry', context)
+ return (context.permissions & Permission.CREATE) !== 0
},
async handler(context, content) {
const contentNames = content.map((node) => node.basename)
- const fileName = getUniqueName(t('files_mindmap', "New mind map.km"), contentNames)
+ const fileName = getUniqueName(t('files_mindmap', 'New mind map.km'), contentNames)
const source = context.encodedSource + '/' + encodeURIComponent(fileName)
const response = await axios({
@@ -264,33 +263,33 @@ var FilesMindMap = {
emit('files:node:created', file)
- OCA.Viewer.openWith('mindmap', { path: file.path });
+ OCA.Viewer.openWith('mindmap', { path: file.path })
},
- });
+ })
},
- setFile: function(file) {
- let filename = file.filename + '';
- let basename = file.basename + '';
+ setFile(file) {
+ const filename = file.filename + ''
+ const basename = file.basename + ''
- this._file.name = basename;
- this._file.root = '/files/' + getCurrentUser()?.uid;
- this._file.dir = dirname(filename);
- this._file.fullName = filename;
+ this._file.name = basename
+ this._file.root = '/files/' + getCurrentUser()?.uid
+ this._file.dir = dirname(filename)
+ this._file.fullName = filename
this._currentContext = {
dir: this._file.dir,
- root: this._file.root
+ root: this._file.root,
}
},
- getSupportedMimetypes: function() {
- var result = [];
- this._extensions.forEach(function(obj){
- result = result.concat(obj.mimes);
- });
- console.debug('Mindmap Mimetypes:', result);
- return result;
+ getSupportedMimetypes() {
+ let result = []
+ this._extensions.forEach(function(obj) {
+ result = result.concat(obj.mimes)
+ })
+ console.debug('Mindmap Mimetypes:', result)
+ return result
},
-};
+}
-export default FilesMindMap;
+export default FilesMindMap
diff --git a/src/mindmapviewer.js b/src/mindmapviewer.js
index 4ab6508..ccd12d3 100644
--- a/src/mindmapviewer.js
+++ b/src/mindmapviewer.js
@@ -1,5 +1,6 @@
+/* global OCA */
import MindMap from './views/MindMap.vue'
-import FilesMindMap from './mindmap'
+import FilesMindMap from './mindmap.js'
OCA.FilesMindMap = FilesMindMap
diff --git a/src/plugins/freemind.js b/src/plugins/freemind.js
index a1d29f1..11ebdab 100644
--- a/src/plugins/freemind.js
+++ b/src/plugins/freemind.js
@@ -1,77 +1,78 @@
+/* global FilesMindMap */
export default {
name: 'freemind',
mimes: ['application/x-freemind'],
encode: null,
- decode: function(data) {
- var self = this;
- return new Promise(function(resolve, reject) {
- try {
- var result = self.toKm(data);
- resolve(result);
- } catch (e) {
- reject(e);
- }
- });
+ decode(data) {
+ const self = this
+ return new Promise(function(resolve, reject) {
+ try {
+ const result = self.toKm(data)
+ resolve(result)
+ } catch (e) {
+ reject(e)
+ }
+ })
},
- markerMap: {
- 'full-1': ['priority', 1],
- 'full-2': ['priority', 2],
- 'full-3': ['priority', 3],
- 'full-4': ['priority', 4],
- 'full-5': ['priority', 5],
- 'full-6': ['priority', 6],
- 'full-7': ['priority', 7],
- 'full-8': ['priority', 8]
- },
- processTopic: function (topic, obj) {
- //处理文本
- obj.data = {
- text: topic.TEXT
- };
- var i;
+ markerMap: {
+ 'full-1': ['priority', 1],
+ 'full-2': ['priority', 2],
+ 'full-3': ['priority', 3],
+ 'full-4': ['priority', 4],
+ 'full-5': ['priority', 5],
+ 'full-6': ['priority', 6],
+ 'full-7': ['priority', 7],
+ 'full-8': ['priority', 8],
+ },
+ processTopic(topic, obj) {
+ // 处理文本
+ obj.data = {
+ text: topic.TEXT,
+ }
+ let i
- // 处理标签
- if (topic.icon) {
- var icons = topic.icon;
- var type;
- if (icons.length && icons.length > 0) {
- for (i in icons) {
- type = this.markerMap[icons[i].BUILTIN];
- if (type) obj.data[type[0]] = type[1];
- }
- } else {
- type = this.markerMap[icons.BUILTIN];
- if (type) obj.data[type[0]] = type[1];
- }
- }
+ // 处理标签
+ if (topic.icon) {
+ const icons = topic.icon
+ let type
+ if (icons.length && icons.length > 0) {
+ for (i in icons) {
+ type = this.markerMap[icons[i].BUILTIN]
+ if (type) obj.data[type[0]] = type[1]
+ }
+ } else {
+ type = this.markerMap[icons.BUILTIN]
+ if (type) obj.data[type[0]] = type[1]
+ }
+ }
- // 处理超链接
- if (topic.LINK) {
- obj.data.hyperlink = topic.LINK;
- }
+ // 处理超链接
+ if (topic.LINK) {
+ obj.data.hyperlink = topic.LINK
+ }
- //处理子节点
- if (topic.node) {
- var tmp = topic.node;
- if (tmp.length && tmp.length > 0) { //多个子节点
- obj.children = [];
+ // 处理子节点
+ if (topic.node) {
+ const tmp = topic.node
+ if (tmp.length && tmp.length > 0) { // 多个子节点
+ obj.children = []
- for (i in tmp) {
- obj.children.push({});
- this.processTopic(tmp[i], obj.children[i]);
- }
+ for (i in tmp) {
+ obj.children.push({})
+ this.processTopic(tmp[i], obj.children[i])
+ }
- } else { //一个子节点
- obj.children = [{}];
- this.processTopic(tmp, obj.children[0]);
- }
- }
- },
- toKm: function (xml) {
- var json = FilesMindMap.Util.xml2json(xml);
- var result = {};
- this.processTopic(json.node, result);
- return result;
- }
+ } else { // 一个子节点
+ obj.children = [{}]
+ this.processTopic(tmp, obj.children[0])
+ }
+ }
+ },
+ toKm(xml) {
+ const json = FilesMindMap.Util.xml2json(xml)
+ const result = {}
+ this.processTopic(json.node, result)
+ return result
+ },
-}
\ No newline at end of file
+}
diff --git a/src/plugins/xmind.js b/src/plugins/xmind.js
index a4419ef..91275fa 100644
--- a/src/plugins/xmind.js
+++ b/src/plugins/xmind.js
@@ -1,106 +1,106 @@
import JSZip from 'jszip'
-import util from '../util'
+import util from '../util.js'
export default {
name: 'xmind',
mimes: ['application/vnd.xmind.workbook'],
encode: null,
- decode: function(data) {
- return this.readDocument(data);
+ decode(data) {
+ return this.readDocument(data)
},
- markerMap : {
- 'priority-1': ['priority', 1],
- 'priority-2': ['priority', 2],
- 'priority-3': ['priority', 3],
- 'priority-4': ['priority', 4],
- 'priority-5': ['priority', 5],
- 'priority-6': ['priority', 6],
- 'priority-7': ['priority', 7],
- 'priority-8': ['priority', 8],
+ markerMap: {
+ 'priority-1': ['priority', 1],
+ 'priority-2': ['priority', 2],
+ 'priority-3': ['priority', 3],
+ 'priority-4': ['priority', 4],
+ 'priority-5': ['priority', 5],
+ 'priority-6': ['priority', 6],
+ 'priority-7': ['priority', 7],
+ 'priority-8': ['priority', 8],
- 'task-start': ['progress', 1],
- 'task-oct': ['progress', 2],
- 'task-quarter': ['progress', 3],
- 'task-3oct': ['progress', 4],
- 'task-half': ['progress', 5],
- 'task-5oct': ['progress', 6],
- 'task-3quar': ['progress', 7],
- 'task-7oct': ['progress', 8],
- 'task-done': ['progress', 9]
- },
- processTopic: function (topic, obj) {
+ 'task-start': ['progress', 1],
+ 'task-oct': ['progress', 2],
+ 'task-quarter': ['progress', 3],
+ 'task-3oct': ['progress', 4],
+ 'task-half': ['progress', 5],
+ 'task-5oct': ['progress', 6],
+ 'task-3quar': ['progress', 7],
+ 'task-7oct': ['progress', 8],
+ 'task-done': ['progress', 9],
+ },
+ processTopic(topic, obj) {
- //处理文本
- obj.data = {
- text: topic.title
- };
+ // 处理文本
+ obj.data = {
+ text: topic.title,
+ }
- // 处理标签
- if (topic.marker_refs && topic.marker_refs.marker_ref) {
- var markers = topic.marker_refs.marker_ref;
- var type;
- if (markers.length && markers.length > 0) {
- for (var i in markers) {
- type = this.markerMap[markers[i].marker_id];
- if (type) obj.data[type[0]] = type[1];
- }
- } else {
- type = this.markerMap[markers.marker_id];
- if (type) obj.data[type[0]] = type[1];
- }
- }
+ // 处理标签
+ if (topic.marker_refs && topic.marker_refs.marker_ref) {
+ const markers = topic.marker_refs.marker_ref
+ let type
+ if (markers.length && markers.length > 0) {
+ for (const i in markers) {
+ type = this.markerMap[markers[i].marker_id]
+ if (type) obj.data[type[0]] = type[1]
+ }
+ } else {
+ type = this.markerMap[markers.marker_id]
+ if (type) obj.data[type[0]] = type[1]
+ }
+ }
- // 处理超链接
- if (topic['xlink:href']) {
- obj.data.hyperlink = topic['xlink:href'];
- }
- //处理子节点
- var topics = topic.children && topic.children.topics;
- var subTopics = topics && (topics.topic || topics[0] && topics[0].topic);
- if (subTopics) {
- var tmp = subTopics;
- if (tmp.length && tmp.length > 0) { //多个子节点
- obj.children = [];
+ // 处理超链接
+ if (topic['xlink:href']) {
+ obj.data.hyperlink = topic['xlink:href']
+ }
+ // 处理子节点
+ const topics = topic.children && topic.children.topics
+ const subTopics = topics && (topics.topic || (topics[0] && topics[0].topic))
+ if (subTopics) {
+ const tmp = subTopics
+ if (tmp.length && tmp.length > 0) { // 多个子节点
+ obj.children = []
- for (var ii in tmp) {
- obj.children.push({});
- this.processTopic(tmp[ii], obj.children[ii]);
- }
+ for (const ii in tmp) {
+ obj.children.push({})
+ this.processTopic(tmp[ii], obj.children[ii])
+ }
- } else { //一个子节点
- obj.children = [{}];
- this.processTopic(tmp, obj.children[0]);
- }
- }
- },
- toKm: function (xml) {
- var json = util.xml2json(xml);
- var result = {};
- var sheet = json.sheet;
- var topic = Array.isArray(sheet) ? sheet[0].topic : sheet.topic;
- this.processTopic(topic, result);
- return result;
- },
- readDocument: function (file) {
- var self = this;
- return new Promise(function(resolve, reject) {
- JSZip.loadAsync(file).then(function(zip){
- var contentFile = zip.file('content.xml');
- if (contentFile != null) {
- contentFile.async('text').then(function(text){
- try {
- var json = self.toKm(text);
- resolve(json);
- } catch (e) {
- reject(e);
- }
- });
- } else {
- reject(new Error('Content document missing'));
- }
- }, function(e) {
- reject(e);
- });
- });
- }
-}
\ No newline at end of file
+ } else { // 一个子节点
+ obj.children = [{}]
+ this.processTopic(tmp, obj.children[0])
+ }
+ }
+ },
+ toKm(xml) {
+ const json = util.xml2json(xml)
+ const result = {}
+ const sheet = json.sheet
+ const topic = Array.isArray(sheet) ? sheet[0].topic : sheet.topic
+ this.processTopic(topic, result)
+ return result
+ },
+ readDocument(file) {
+ const self = this
+ return new Promise(function(resolve, reject) {
+ JSZip.loadAsync(file).then(function(zip) {
+ const contentFile = zip.file('content.xml')
+ if (contentFile != null) {
+ contentFile.async('text').then(function(text) {
+ try {
+ const json = self.toKm(text)
+ resolve(json)
+ } catch (e) {
+ reject(e)
+ }
+ })
+ } else {
+ reject(new Error('Content document missing'))
+ }
+ }, function(e) {
+ reject(e)
+ })
+ })
+ },
+}
diff --git a/src/public.js b/src/public.js
index 3a3de6f..469a6ac 100644
--- a/src/public.js
+++ b/src/public.js
@@ -1,45 +1,40 @@
-import FilesMindMap from './mindmap'
-import logger from './logger'
+/* global OCA */
+import FilesMindMap from './mindmap.js'
+import logger from './logger.js'
-import { getSharingToken, isPublicShare } from '@nextcloud/sharing/public'
+import { isPublicShare } from '@nextcloud/sharing/public'
if (isPublicShare()) {
- OCA.FilesMindMap = FilesMindMap;
- FilesMindMap.init();
-
- if (FilesMindMap.isMindmapPublic()) {
- window.addEventListener('DOMContentLoaded', function() {
- var sharingToken = getSharingToken();
- var downloadUrl = OC.generateUrl('/s/{token}/download', {token: sharingToken});
- var viewer = OCA.FilesMindMap;
-
-
- const contentElmt = document.getElementById('files-public-content');
- const footerElmt = document.querySelector('body > footer') || document.querySelector('#app-content > footer');
- if (contentElmt) {
- if (OCA.Viewer) {
- contentElmt.innerHTML = '';
- OCA.Viewer.setRootElement('#files-public-content')
- OCA.Viewer.open({ path: '/' });
-
- footerElmt.style.display = 'none';
-
- // This is an ugly implementation, need to remove the top margin after viewer creates the iframe
- setTimeout(() => {
- const frameElmt = document.querySelector('#viewer > iframe');
- if (frameElmt) {
- frameElmt.style.marginTop = '0';
- }
- }, 1000);
-
- } else {
- logger.error('Viewer is not available, cannot preview mindmap');
- }
- }
-
- });
- }
-
-
- console.log('files_mindmap public.js loaded');
-}
\ No newline at end of file
+ OCA.FilesMindMap = FilesMindMap
+ FilesMindMap.init()
+
+ if (FilesMindMap.isMindmapPublic()) {
+ window.addEventListener('DOMContentLoaded', function() {
+ const contentElmt = document.getElementById('files-public-content')
+ const footerElmt = document.querySelector('body > footer') || document.querySelector('#app-content > footer')
+ if (contentElmt) {
+ if (OCA.Viewer) {
+ contentElmt.innerHTML = ''
+ OCA.Viewer.setRootElement('#files-public-content')
+ OCA.Viewer.open({ path: '/' })
+
+ footerElmt.style.display = 'none'
+
+ // This is an ugly implementation, need to remove the top margin after viewer creates the iframe
+ setTimeout(() => {
+ const frameElmt = document.querySelector('#viewer > iframe')
+ if (frameElmt) {
+ frameElmt.style.marginTop = '0'
+ }
+ }, 1000)
+
+ } else {
+ logger.error('Viewer is not available, cannot preview mindmap')
+ }
+ }
+
+ })
+ }
+
+ console.debug('files_mindmap public.js loaded')
+}
diff --git a/src/util.js b/src/util.js
index 8a04603..526c619 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,104 +1,101 @@
export default {
- base64Encode: function(string) {
+ base64Encode(string) {
return btoa(encodeURIComponent(string).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode(parseInt(p1, 16))
}))
},
- base64Decode: function(base64) {
+ base64Decode(base64) {
try {
return decodeURIComponent(Array.prototype.map.call(atob(base64), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
- }).join(''));
+ }).join(''))
} catch (e) {
- var binary = atob(base64);
- var array = new Uint8Array(binary.length);
- for (var i = 0; i < binary.length; i++) {
- array[i] = binary.charCodeAt(i);
+ const binary = atob(base64)
+ const array = new Uint8Array(binary.length)
+ for (let i = 0; i < binary.length; i++) {
+ array[i] = binary.charCodeAt(i)
}
- return new Blob([array]);
+ return new Blob([array])
}
},
- jsVar: function (s) {
- return String(s || '').replace(/-/g,"_");
- },
- toArray: function (obj){
- if (!Array.isArray(obj)) {
- return [obj];
- }
- return obj;
- },
- parseNode: function (node) {
- if (!node) return null;
- var self = this;
- var txt = '', obj = null, att = null;
+ jsVar(s) {
+ return String(s || '').replace(/-/g, '_')
+ },
+ toArray(obj) {
+ if (!Array.isArray(obj)) {
+ return [obj]
+ }
+ return obj
+ },
+ parseNode(node) {
+ if (!node) return null
+ const self = this
+ let txt = ''; let obj = null; let att = null
- if (node.childNodes) {
- if (node.childNodes.length > 0) {
- node.childNodes.forEach(function(cn) {
- var cnt = cn.nodeType, cnn = self.jsVar(cn.localName || cn.nodeName);
- var cnv = cn.text || cn.nodeValue || '';
+ if (node.childNodes) {
+ if (node.childNodes.length > 0) {
+ node.childNodes.forEach(function(cn) {
+ const cnt = cn.nodeType; const cnn = self.jsVar(cn.localName || cn.nodeName)
+ const cnv = cn.text || cn.nodeValue || ''
- /* comment */
- if (cnt == 8) {
- return; // ignore comment node
- }
- /* white-space */
- else if (cnt == 3 || cnt == 4 || !cnn) {
- if (cnv.match(/^\s+$/)) {
- return;
- }
- txt += cnv.replace(/^\s+/, '').replace(/\s+$/, '');
- } else {
- obj = obj || {};
- if (obj[cnn]) {
- if (!obj[cnn].length) {
- obj[cnn] = self.toArray(obj[cnn]);
- }
- obj[cnn] = self.toArray(obj[cnn]);
+ if (cnt === 8) {
+ // comment node — ignore
+ } else if (cnt === 3 || cnt === 4 || !cnn) {
+ // white-space
+ if (cnv.match(/^\s+$/)) {
+ return
+ }
+ txt += cnv.replace(/^\s+/, '').replace(/\s+$/, '')
+ } else {
+ obj = obj || {}
+ if (obj[cnn]) {
+ if (!obj[cnn].length) {
+ obj[cnn] = self.toArray(obj[cnn])
+ }
+ obj[cnn] = self.toArray(obj[cnn])
- obj[cnn].push(self.parseNode(cn, true));
- } else {
- obj[cnn] = self.parseNode(cn);
- }
- }
- });
- }
- }
- if (node.attributes && node.tagName !='title') {
- if (node.attributes.length > 0) {
- att = {}; obj = obj || {};
- node.attributes.forEach = [].forEach.bind(node.attributes);
- node.attributes.forEach(function (at) {
- var atn = self.jsVar(at.name), atv = at.value;
- att[atn] = atv;
- if (obj[atn]) {
- obj[cnn] = this.toArray(obj[cnn]);
- obj[atn][obj[atn].length] = atv;
- }
- else {
- obj[atn] = atv;
- }
- });
- }
- }
- if (obj) {
- obj = Object.assign({}, (txt != '' ? new String(txt) : {}), obj || {});
- txt = (obj.text) ? ([obj.text || '']).concat([txt]) : txt;
- if (txt) obj.text = txt;
- txt = '';
- }
- var out = obj || txt;
- return out;
- },
- parseXML: function (xml) {
- var root = (xml.nodeType == 9) ? xml.documentElement : xml;
- return this.parseNode(root, true);
- },
- xml2json: function (str) {
- var domParser = new DOMParser();
- var dom = domParser.parseFromString(str, 'application/xml');
+ obj[cnn].push(self.parseNode(cn, true))
+ } else {
+ obj[cnn] = self.parseNode(cn)
+ }
+ }
+ })
+ }
+ }
+ if (node.attributes && node.tagName !== 'title') {
+ if (node.attributes.length > 0) {
+ att = {}; obj = obj || {}
+ node.attributes.forEach = [].forEach.bind(node.attributes)
+ node.attributes.forEach(function(at) {
+ const atn = self.jsVar(at.name); const atv = at.value
+ att[atn] = atv
+ if (obj[atn]) {
+ obj[atn] = self.toArray(obj[atn])
+ obj[atn][obj[atn].length] = atv
+ } else {
+ obj[atn] = atv
+ }
+ })
+ }
+ }
+ if (obj) {
+ obj = Object.assign({}, (txt !== '' ? String(txt) : {}), obj || {})
+ txt = (obj.text) ? ([obj.text || '']).concat([txt]) : txt
+ if (txt) obj.text = txt
+ txt = ''
+ }
+ const out = obj || txt
+ return out
+ },
+ parseXML(xml) {
+ const root = (xml.nodeType === 9) ? xml.documentElement : xml
+ return this.parseNode(root, true)
+ },
+ xml2json(str) {
+ const domParser = new DOMParser()
+ const dom = domParser.parseFromString(str, 'application/xml')
- var json = this.parseXML(dom);
- return json;
- },
-}
\ No newline at end of file
+ const json = this.parseXML(dom)
+ return json
+ },
+}
diff --git a/src/viewer.js b/src/viewer.js
index 50859b7..201df53 100644
--- a/src/viewer.js
+++ b/src/viewer.js
@@ -1,303 +1,308 @@
-
+/* global $, minder, Base64, jsPDF, angular */
+/* eslint-disable @nextcloud/no-deprecations */
/**
* Checks if the page is displayed in an iframe. If not redirect to /.
- **/
-function redirectIfNotDisplayedInFrame () {
+ */
+function redirectIfNotDisplayedInFrame() {
try {
if (window.frameElement) {
- return;
+ return
}
} catch (e) {}
- window.location.href = '/';
+ window.location.href = '/'
}
redirectIfNotDisplayedInFrame();
(function() {
- var t = function(msg) {
- return window.parent.t('files_mindmap', msg);
- };
+ const t = function(msg) {
+ return window.parent.t('files_mindmap', msg)
+ }
- var lang = window.lang ||
- (document.getElementById("viewer") && document.getElementById("viewer").getAttribute("lang")) ||
- 'en';
+ const lang = window.lang
+ || (document.getElementById('viewer') && document.getElementById('viewer').getAttribute('lang'))
+ || 'en'
- var MindMap = {
+ const MindMap = {
_changed: false,
_autoSaveTimer: null,
_clearStatusMessageTimer: null,
_loadStatus: false,
- init: function() {
- var self = this;
+ init() {
+ const self = this
angular.module('mindmap', ['kityminderEditor'])
- .config(function (configProvider) {
- configProvider.set('lang', lang);
+ .config(function(configProvider) {
+ configProvider.set('lang', lang)
})
.controller('MainController', function($scope) {
$scope.initEditor = function(editor, minder) {
- window.editor = editor;
- window.minder = minder;
+ window.editor = editor
+ window.minder = minder
- self.initHotkey();
- self.bindEvent();
- self.loadData();
- self.loadAutoSaveStatus();
- self.startAutoSaveTimer();
+ self.initHotkey()
+ self.bindEvent()
+ self.loadData()
+ self.loadAutoSaveStatus()
+ self.startAutoSaveTimer()
minder.on('contentchange', function() {
- self._changed = true;
- });
- };
- });
+ self._changed = true
+ })
+ }
+ })
angular.module('ui.colorpicker')
- .config(function (localizeProvider) {
- localizeProvider.setDefaultLang('en-us');
- }) ;
+ .config(function(localizeProvider) {
+ localizeProvider.setDefaultLang('en-us')
+ })
},
- initHotkey: function() {
- var self = this;
+ initHotkey() {
+ const self = this
$(document).keydown(function(e) {
- if((e.ctrlKey || e.metaKey) && e.which === 83){
- self.save();
- e.preventDefault();
- return false;
+ if ((e.ctrlKey || e.metaKey) && e.which === 83) {
+ self.save()
+ e.preventDefault()
+ return false
}
- });
+ })
},
- bindEvent: function() {
- var self = this;
- $('#export-png').click(function(){
- self.exportPNG();
- });
- $('#export-svg').click(function(){
- self.exportSVG();
- });
- $('#export-pdf').click(function(){
- self.exportPDF();
- });
- $('#export-markdown').click(function(){
- self.exportMarkdown();
- });
- $('#export-text').click(function(){
- self.exportText();
- });
+ bindEvent() {
+ const self = this
+ $('#export-png').click(function() {
+ self.exportPNG()
+ })
+ $('#export-svg').click(function() {
+ self.exportSVG()
+ })
+ $('#export-pdf').click(function() {
+ self.exportPDF()
+ })
+ $('#export-markdown').click(function() {
+ self.exportMarkdown()
+ })
+ $('#export-text').click(function() {
+ self.exportText()
+ })
$('#save-button').click(function() {
- self.save();
- });
+ self.save()
+ })
},
- close: function() {
- var self = this;
- var doHide = function() {
+ close() {
+ const self = this
+ const doHide = function() {
if (self._autoSaveTimer !== null) {
- clearInterval(self._autoSaveTimer);
+ clearInterval(self._autoSaveTimer)
}
- window.parent.OCA.FilesMindMap.hide();
+ window.parent.OCA.FilesMindMap.hide()
}
if (this._changed && window.parent.OCA.FilesMindMap._file.supportedWrite) {
- var result = window.confirm(t('The file has not been saved. Is it saved?'));
+ const result = window.confirm(t('The file has not been saved. Is it saved?'))
if (result) {
- self.save(function(status){
+ self.save(function(status) {
if (status) {
- doHide();
+ doHide()
}
- });
+ })
} else {
- doHide();
+ doHide()
}
} else {
- doHide();
+ doHide()
}
},
- showMessage: function(msg, delay) {
- return window.parent.OCA.FilesMindMap.showMessage(msg, delay);
+ showMessage(msg, delay) {
+ return window.parent.OCA.FilesMindMap.showMessage(msg, delay)
},
- hideMessage: function(id) {
- return window.parent.OCA.FilesMindMap.hideMessage(id);
+ hideMessage(id) {
+ return window.parent.OCA.FilesMindMap.hideMessage(id)
},
- setStatusMessage: function(msg) {
- this.showMessage(msg);
+ setStatusMessage(msg) {
+ this.showMessage(msg)
},
- updateSaveButtonInfo: function(msg) {
- $('#save-button').html(msg);
+ updateSaveButtonInfo(msg) {
+ $('#save-button').html(msg)
},
- restoreSaveButtonInfo: function(time) {
- var self = this;
- setTimeout(function(){
- self.updateSaveButtonInfo(t('Save'));
- }, time);
+ restoreSaveButtonInfo(time) {
+ const self = this
+ setTimeout(function() {
+ self.updateSaveButtonInfo(t('Save'))
+ }, time)
},
- save: function(callback) {
- var self = this;
+ save(onComplete) {
+ const self = this
if (self._changed) {
- self.updateSaveButtonInfo(t('Saving...'));
- var data = JSON.stringify(minder.exportJson());
- window.parent.OCA.FilesMindMap.save(data, function(msg){
- self.updateSaveButtonInfo(msg);
- self._changed = false;
- self.restoreSaveButtonInfo(3000);
- if (undefined !== callback) {
- callback(true, msg);
+ self.updateSaveButtonInfo(t('Saving...'))
+ const data = JSON.stringify(minder.exportJson())
+ window.parent.OCA.FilesMindMap.save(data, function(msg) {
+ self.updateSaveButtonInfo(msg)
+ self._changed = false
+ self.restoreSaveButtonInfo(3000)
+ if (undefined !== onComplete) {
+ onComplete(true, msg)
}
- }, function(msg){
- self.updateSaveButtonInfo(msg);
- self.restoreSaveButtonInfo(3000);
- if (undefined !== callback) {
- callback(false, msg);
+ }, function(msg) {
+ self.updateSaveButtonInfo(msg)
+ self.restoreSaveButtonInfo(3000)
+ if (undefined !== onComplete) {
+ onComplete(false, msg)
}
- });
- self.restoreSaveButtonInfo(6000);
+ })
+ self.restoreSaveButtonInfo(6000)
}
},
- startAutoSaveTimer: function() {
- var self = this;
+ startAutoSaveTimer() {
+ const self = this
if (self._autoSaveTimer != null) {
- clearInterval(self._autoSaveTimer);
- self._autoSaveTimer = null;
+ clearInterval(self._autoSaveTimer)
+ self._autoSaveTimer = null
}
self._autoSaveTimer = setInterval(function() {
if (self.getAutoSaveStatus()) {
/* When file is readonly, autosave will stop working */
if (window.parent.OCA.FilesMindMap._file.writeable) {
- self.save();
+ self.save()
}
}
- }, 10000);
+ }, 10000)
},
- getAutoSaveStatus: function() {
- var status = $('#autosave-checkbox').is(':checked');
+ getAutoSaveStatus() {
+ const status = $('#autosave-checkbox').is(':checked')
if (window.localStorage) {
- localStorage.setItem('apps.files_mindmap.autosave', status);
+ localStorage.setItem('apps.files_mindmap.autosave', status)
}
- return status;
+ return status
},
- loadAutoSaveStatus: function() {
- var status = true;
+ loadAutoSaveStatus() {
+ let status = true
if (window.localStorage) {
if (localStorage.getItem('apps.files_mindmap.autosave') === 'false') {
- status = false;
+ status = false
}
}
- $('#autosave-checkbox').prop("checked", status);
+ $('#autosave-checkbox').prop('checked', status)
},
- loadData: function() {
- var self = this;
- window.parent.OCA.FilesMindMap.load(function(data){
- var obj = {"root":
- {"data":
- {"id":"bopmq"+String(Math.floor(Math.random() * 9e15)).substr(0, 7),
- "created":(new Date()).getTime(),
- "text":t('Main Topic')
+ loadData() {
+ const self = this
+ window.parent.OCA.FilesMindMap.load(function(data) {
+ let obj = {
+ root:
+ {
+ data:
+ {
+ id: 'bopmq' + String(Math.floor(Math.random() * 9e15)).substr(0, 7),
+ created: (new Date()).getTime(),
+ text: t('Main Topic'),
},
- "children":[]
+ children: [],
},
- "template":"default",
- "theme":"fresh-blue",
- "version":"1.4.43"
- };
+ template: 'default',
+ theme: 'fresh-blue',
+ version: '1.4.43',
+ }
/* 新生成的空文件 */
if (data !== ' ') {
try {
- obj = JSON.parse(data);
- } catch (e){
- window.alert(t('This file is not a valid mind map file and may cause file ' +
- 'corruption if you continue editing.'));
+ obj = JSON.parse(data)
+ } catch (e) {
+ window.alert(t('This file is not a valid mind map file and may cause file '
+ + 'corruption if you continue editing.'))
}
}
- minder.importJson(obj);
+ minder.importJson(obj)
if (data === ' ') {
- self._changed = true;
- self.save();
+ self._changed = true
+ self.save()
}
- self._loadStatus = true;
- self._changed = false;
+ self._loadStatus = true
+ self._changed = false
/* When file is readonly, hide autosave checkbox */
if (!window.parent.OCA.FilesMindMap._file.writeable) {
- $('#autosave-div').hide();
+ $('#autosave-div').hide()
}
/* When extension cannot write, hide save checkbox */
if (!window.parent.OCA.FilesMindMap._file.supportedWrite) {
- $('#save-div').hide();
+ $('#save-div').hide()
}
- }, function(msg){
- self._loadStatus = false;
- window.alert(t('Load file fail!') + msg);
- window.parent.OCA.FilesMindMap.hide();
- });
+ }, function(msg) {
+ self._loadStatus = false
+ window.alert(t('Load file fail!') + msg)
+ window.parent.OCA.FilesMindMap.hide()
+ })
},
- isDataSchema: function (url) {
- var i = 0,
- ii = url.length;
+ isDataSchema(url) {
+ let i = 0
+ const ii = url.length
while (i < ii && url[i].trim() === '') {
- i++;
+ i++
}
- return url.substr(i, 5).toLowerCase() === 'data:';
+ return url.substr(i, 5).toLowerCase() === 'data:'
},
- download: function(url, filename) {
- var obj = document.createElement('a');
- obj.href = url;
- obj.download = filename;
- obj.dataset.downloadurl = url;
- document.body.appendChild(obj);
- obj.click();
- document.body.removeChild(obj);
+ download(url, filename) {
+ const obj = document.createElement('a')
+ obj.href = url
+ obj.download = filename
+ obj.dataset.downloadurl = url
+ document.body.appendChild(obj)
+ obj.click()
+ document.body.removeChild(obj)
},
- exportPNG: function () {
- var self = this;
- minder.exportData('png').then(function (data) {
- self.download(data, 'export.png');
- }, function (data){
- console.error('export png fail', data);
- });
+ exportPNG() {
+ const self = this
+ minder.exportData('png').then(function(data) {
+ self.download(data, 'export.png')
+ }, function(data) {
+ console.error('export png fail', data)
+ })
},
- exportSVG: function () {
- var self = this;
- minder.exportData('svg').then(function (data) {
- var url = 'data:image/svg+xml;base64,' + Base64.encode(data);
- self.download(url, 'export.svg');
- }, function (data){
- console.error('export svg fail', data);
- });
+ exportSVG() {
+ const self = this
+ minder.exportData('svg').then(function(data) {
+ const url = 'data:image/svg+xml;base64,' + Base64.encode(data)
+ self.download(url, 'export.svg')
+ }, function(data) {
+ console.error('export svg fail', data)
+ })
},
- exportMarkdown: function () {
- var self = this;
- minder.exportData('markdown').then(function (data) {
- var url = 'data:text/markdown;base64,' + Base64.encode(data);
- self.download(url, 'export.md');
- }, function (data){
- console.error('export markdown fail', data);
- });
+ exportMarkdown() {
+ const self = this
+ minder.exportData('markdown').then(function(data) {
+ const url = 'data:text/markdown;base64,' + Base64.encode(data)
+ self.download(url, 'export.md')
+ }, function(data) {
+ console.error('export markdown fail', data)
+ })
},
- exportText: function () {
- var self = this;
- minder.exportData('text').then(function (data) {
- var url = 'data:text/plain;base64,' + Base64.encode(data);
- self.download(url, 'export.txt');
- }, function (data){
- console.error('export text fail', data);
- });
+ exportText() {
+ const self = this
+ minder.exportData('text').then(function(data) {
+ const url = 'data:text/plain;base64,' + Base64.encode(data)
+ self.download(url, 'export.txt')
+ }, function(data) {
+ console.error('export text fail', data)
+ })
},
- exportPDF: function () {
- var self = this;
- minder.exportData('png').then(function (data) {
- var pdf = new jsPDF('p', 'mm', 'a4', false);
- //pdf.addImage(data, 'png', 100, 200, 280, 210, undefined, 'none');
- pdf.addImage(data, 'PNG', 5, 10, 200, 0, undefined, 'SLOW');
- self.download(pdf.output('datauristring'), 'export.pdf');
- }, function (data){
- console.error('export png fail', data);
- });
- }
- };
+ exportPDF() {
+ const self = this
+ minder.exportData('png').then(function(data) {
+ // eslint-disable-next-line new-cap
+ const pdf = new jsPDF('p', 'mm', 'a4', false)
+ // pdf.addImage(data, 'png', 100, 200, 280, 210, undefined, 'none');
+ pdf.addImage(data, 'PNG', 5, 10, 200, 0, undefined, 'SLOW')
+ self.download(pdf.output('datauristring'), 'export.pdf')
+ }, function(data) {
+ console.error('export png fail', data)
+ })
+ },
+ }
- window.MindMap = MindMap;
-})();
+ window.MindMap = MindMap
+})()
-window.MindMap.init();
+window.MindMap.init()
diff --git a/src/views/MindMap.vue b/src/views/MindMap.vue
index eac9efd..f6aa0af 100644
--- a/src/views/MindMap.vue
+++ b/src/views/MindMap.vue
@@ -1,25 +1,22 @@
+ :src="iframeSrc"
+ @load="onIFrameLoaded" />
@@ -80,4 +76,4 @@ iframe {
margin-top: var(--header-height);
position: absolute;
}
-
\ No newline at end of file
+
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..66ba211
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vitest/config'
+import vue from '@vitejs/plugin-vue2'
+
+export default defineConfig({
+ plugins: [vue()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ server: {
+ deps: {
+ // cancelable-promise is CJS; inline it so Vite handles interop
+ inline: ['cancelable-promise'],
+ },
+ },
+ },
+})