Skip to content

Editor integrations ​

SoFinder ships sofinder-picker.js as a small framework-independent ES module. Every integration uses the same picker URL and returns the same versioned entry object; editor-specific code never calls private React APIs.

The resolved object follows the published picker entry JSON Schema and always includes resource, path, name, url, mimeType, size, modifiedAt, width, height and capabilities. Dimensions are null for non-images. Consumers must ignore additional 1.x fields. The message envelope has its own JSON Schema.

Upload responses and new picker consumers can use the additive Asset Reference 1.0 Schema. It adds a version fingerprint, download URL, optional stable asset ID, alternative text, responsive variants and explicit embeddable capability without removing the legacy entry.

js
import { openPicker } from '/sofinder/assets/sofinder-picker.js'

const entry = await openPicker({
  baseUrl: '/sofinder/browser',
  kind: 'image',
  resource: 'Images',
  path: 'articles/2026'
})

The picker opens as a popup. Results are accepted only when the popup window, origin, protocol version and random request ID all match. SoFinder does not use a wildcard postMessage target. Same-origin works without configuration. A cross-origin host must be listed exactly in picker.allowed_origins; login, resource ACLs and selection validation still run on the picker server.

When resource is provided, picker mode treats it as a resource boundary rather than only an initial location. Other resources are excluded from navigation and search, and the SDK ignores any returned entry whose resource does not match. Pass lockResource: false when the resource should only be the initial location and all other authorized resources should remain visible. Direct picker URLs can use resourceLock=0. Manager deep links continue to use type only as their initial resource.

SoEditor ​

SoEditor pairs HTML editing with SoFinder asset management. Use the official @soeditor/adapter-sofinder adapter for images, attachments, video files and posters.

Install matching versions of @soeditor/editor, @soeditor/presets, @soeditor/file-manager and @soeditor/adapter-sofinder (this example targets 1.4.0). Add <textarea id="content"></textarea> to your page, then use this module in your application build:

js
import { createClassicEditor as createOptionalEditor } from '@soeditor/editor/cms/optional'
import '@soeditor/editor/cms/styles.css'
import { cmsRuntimePreset } from '@soeditor/presets/cms-runtime'
import { FileManagerPlugin, fileManagerServiceToken } from '@soeditor/file-manager'
import { SoFinderAdapter } from '@soeditor/adapter-sofinder'
import { openPicker } from '/sofinder/assets/sofinder-picker.js'

const editor = await createOptionalEditor(document.querySelector('#content'), {
  plugins: [...cmsRuntimePreset.plugins, FileManagerPlugin],
  toolbar: ['undo', 'redo', 'bold', 'italic', 'image-browse', 'file-link', 'cmsVideo'],
})

editor.editor.services.register(fileManagerServiceToken, new SoFinderAdapter({
  async pick({ kind, accept }) {
    try {
      const entry = await openPicker({
        baseUrl: '/sofinder/browser',
        kind: kind === 'image' ? 'image' : 'file',
        resource: kind === 'image' ? 'Images' : 'Files',
      })
      const mime = entry.mimeType?.toLowerCase() ?? ''
      if (accept?.length && !accept.some(type => type.endsWith('/*')
        ? mime.startsWith(type.slice(0, -1).toLowerCase())
        : mime === type.toLowerCase())) {
        throw new Error('The selected file does not match the requested format.')
      }
      if (entry.capabilities.embeddable === false) {
        throw new Error('The selected file cannot be embedded.')
      }
      return {
        url: entry.url,
        name: entry.name,
        ...(entry.mimeType != null ? { mimeType: entry.mimeType } : {}),
        ...(entry.assetId != null ? { assetId: entry.assetId } : {}),
        ...(entry.alt != null ? { alt: entry.alt } : {}),
        ...(entry.width != null ? { width: entry.width } : {}),
        ...(entry.height != null ? { height: entry.height } : {}),
      }
    } catch (error) {
      if (error instanceof DOMException && error.name === 'AbortError') return null
      throw error
    }
  },
}))

Replace the browser/asset routes and Images / Files with your own deployment. media requests use the file picker; the bridge checks the requested MIME types before returning a selection. Closing the popup cancels without inserting content; other failures remain errors. Null dimensions are omitted and explicit empty alternative text is preserved. Keep cmsRuntimePreset.plugins when supplying an explicit plugin list.

This example selects existing assets. Direct paste/drop uploads need a separately configured SoFinderUploadAdapter and the host upload SDK. Authentication, resource permissions and stable delivery URLs remain the responsibility of your deployment. See the SoEditor integration guide for its editor-side setup.

CKEditor 5 ​

Keep CKEditor itself in the host application's build. Add a button using the editor's normal UI system, then pass the editor instance to SoFinder:

js
import { selectForCkeditor5 } from '/sofinder/assets/sofinder-picker.js'

button.addEventListener('click', () => selectForCkeditor5(editor, {
  baseUrl: '/sofinder/browser',
  resource: 'Images',
  language: 'en'
}))

The adapter executes CKEditor 5's public insertImage and alternative-text commands. When the asset catalog is enabled, picker and upload insertion preserve explicit alt="", resolve altTranslations for language/locale, use the asset default alt next, and otherwise fall back to the extension-free filename. Translation keys are normalized BCP 47-style tags such as en, zh-cn and fr-ca; the metadata API accepts up to 20. Pass locale: 'en' to an upload adapter. Responsive variants, dimensions and data-sofinder-asset-id are also carried where the editor model supports them. Configure its Image plugin and follow the official self-hosted installation and licensing guide. The legacy CKEditor 4 callback and quick-upload endpoints remain available.

For local selection, paste and desktop drop uploads, install the official adapter without bundling CKEditor into SoFinder:

js
import { createCkeditor5UploadPlugin } from '/sofinder/assets/sofinder-ckeditor5.js'

ClassicEditor.create(element, {
  extraPlugins: [createCkeditor5UploadPlugin({
    apiBase: '/sofinder/api', csrfToken, resource: 'Images'
  })]
})

The adapter uses the public CKEditor UploadAdapter contract and the shared sofinder-sdk.js upload task. Upload progress, abort, retry, whole/chunked transfer and ask|rename|overwrite|skip conflicts therefore behave the same in every editor. A Private resource is rejected unless it has a stable, authorization-preserving delivery URL; a temporary signed URL is never treated as embeddable content. The returned value is a constructible CKEditor plugin; do not invoke it manually or wrap it in another plugin class.

TinyMCE ​

Register the SoFinder plugin before tinymce.init() and add sofinder to the plugin and toolbar lists:

js
import { registerTinyMce } from '/sofinder/assets/sofinder-picker.js'

registerTinyMce(tinymce, {
  baseUrl: '/sofinder/browser',
  resource: 'Images'
})

tinymce.init({
  selector: '#editor',
  plugins: 'sofinder link image',
  toolbar: 'undo redo | bold italic | image sofinder'
})

The adapter inserts an encoded <img> element through TinyMCE's public API. See the TinyMCE deployment guide for CDN keys or self-hosting.

Direct upload adapters are separate ESM entries:

js
import { createTinyMceUploadIntegration } from '/sofinder/assets/sofinder-tinymce.js'
// TipTap: sofinder-tiptap.js; Quill: sofinder-quill.js; wangEditor: sofinder-wangeditor.js; Jodit: sofinder-jodit.js

Create the TinyMCE integration in its setup callback and forward images_upload_handler to it. This lets the bridge apply the full asset attributes after TinyMCE creates the image node instead of losing everything except the URL.

All accept apiBase, csrfToken, resource, optional dynamic path, conflict strategy, default-alt callback, task observer and error callback. The generic Markdown and form bindings are exported by sofinder-editors.js.

TipTap and Quill ​

TipTap requires its Image extension:

js
import { selectForTiptap } from '/sofinder/assets/sofinder-picker.js'

await selectForTiptap(editor, {
  baseUrl: '/sofinder/browser',
  resource: 'Images'
})

For Quill, initialize a toolbar containing the image action, then replace that handler:

js
import { registerQuill } from '/sofinder/assets/sofinder-picker.js'

registerQuill(quill, {
  baseUrl: '/sofinder/browser',
  resource: 'Images'
})

wangEditor 5 ​

Use the editor's public node API for an external picker button, and its MENU_CONF.uploadImage.customUpload hook for local selection, paste and drop:

js
import { selectForWangEditor } from '/sofinder/assets/sofinder-picker.js'
import { createWangEditorUploadIntegration } from '/sofinder/assets/sofinder-wangeditor.js'

const editorConfig = {
  MENU_CONF: {
    uploadImage: createWangEditorUploadIntegration({
      apiBase: '/sofinder/api', csrfToken, resource: 'Images'
    })
  }
}

button.addEventListener('click', () => selectForWangEditor(editor, {
  baseUrl: '/sofinder/browser', resource: 'Images', language: 'en'
}))

To make wangEditor's own image toolbar action open SoFinder directly, assign createWangEditorPickerIntegration(options) to MENU_CONF.uploadImage instead. The bridge follows wangEditor 5's documented insertNode, customUpload and customBrowseAndUpload contracts and does not bundle the editor runtime.

Jodit 4 ​

Pass the SoFinder uploader configuration to Jodit, then use an external button for the asset picker:

js
import { selectForJodit } from '/sofinder/assets/sofinder-picker.js'
import { createJoditUploadIntegration } from '/sofinder/assets/sofinder-jodit.js'

const editor = Jodit.make('#editor', {
  uploader: createJoditUploadIntegration({
    apiBase: '/sofinder/api', csrfToken, resource: 'Images'
  })
})

button.addEventListener('click', () => selectForJodit(editor, {
  baseUrl: '/sofinder/browser', resource: 'Images', language: 'en'
}))

Jodit's native image dialog, paste and drop paths use its documented custom uploader hooks. Picker and upload insertion use createInside.element() and s.insertImage() so full asset attributes are retained without bundling Jodit.

Plain forms ​

selectForInput(input, options) writes the selected URL and emits bubbling input and change events. Use kind: 'file' for documents or kind: 'image' for browser-embeddable images.

Markdown editors ​

selectForMarkdown(textarea, options) inserts at the current selection. Image selections use ![name](<url>); other files use [name](<url>). It emits normal input and change events and does not depend on one Markdown editor.

Local integration matrix ​

Run the project under examples/symfony, sign in with demo / demo, then open /integrations. It exercises CKEditor 5, TinyMCE 8, TipTap, Quill 2, wangEditor 5, Jodit 4 and a plain input against the local SoFinder checkout. Third-party editors load from their documented CDN endpoints; the picker SDK and backend remain local. Review each editor's license and replace demonstration keys before deployment.

The integration demo pins every third-party CDN version. The weekly opt-in Editor network compatibility workflow opens the page in Chromium and waits for the real CKEditor, TinyMCE, TipTap, Quill, wangEditor and Jodit instances; it does not treat script downloads or placeholder DOM as successful startup.

Deep links use type and path, for example:

text
/sofinder/browser?uiMode=manager&type=Images&path=articles/2026

The browser keeps these parameters synchronized as users navigate, so a current folder can be bookmarked or copied without exposing a filesystem path.

Released under the MIT License.