UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

666 lines (550 loc) 17.8 kB
import { forEach, map, isString, Registry, platform } from '../util' import { DocumentSchema, EditingBehavior } from '../model' import ComponentRegistry from './ComponentRegistry' import FontAwesomeIconProvider from './FontAwesomeIconProvider' import LabelProvider from './DefaultLabelProvider' import DefaultCommandManager from './CommandManager' import DefaultDragManager from './DragManager' import DefaultFileManager from './FileManager' import DefaultGlobalEventHandler from './GlobalEventHandler' import DefaultKeyboardManager from './KeyboardManager' import DefaultMacroManager from './MacroManager' import DefaultMarkersManager from './MarkersManager' import DefaultSurfaceManager from './SurfaceManager' import DefaultSaveHandler from '../packages/persistence/SaveHandlerStub' /** Default Configurator for Substance editors. It provides an API for adding nodes to the schema, components, commands and tools etc. @class @example ```js let configurator = new Configurator() configurator.addNode(Heading) configurator.addComponent('heading', HeadingComponent) ``` To modularize configuration, package definitions can be imported. ```js configurator.import(ParagraphPackage) ``` You can create your own extensions that way. ```js const AlienPackage = { name: 'alien' configure: function(config) { config.addNode(AlienNode) config.addComponent('alien', AlienComponent) config.addCommand('add-alien', AddAlienCommand) config.addTool('add-alien', AddAlienTool) } } ``` From within a package, another package can be imported. This provides a simple mechanism to model dependencies between packages. Just make sure you don't run into cyclic dependencies as there is no checking for that at the moment. */ class Configurator { constructor() { this.config = { schema: {}, nodes: {}, tools: {}, components: {}, converters: {}, importers: {}, exporters: {}, fileProxies: [], commands: {}, commandGroups: {}, toolPanels: {}, editingBehaviors: [], macros: [], managers: {}, dropHandlers: [], keyboardShortcuts: [], icons: {}, labels: {}, lang: 'en_US', editorOptions: [], CommandManagerClass: DefaultCommandManager, DragManagerClass: DefaultDragManager, SaveHandlerClass: null, } } // Record phase API // ------------------------ /** Defines the document schema for this configuration. @param {DocumentSchema} schema A schema to be used for articles created from this configuration. */ defineSchema(schema) { if (schema.ArticleClass) { console.warn('DEPRECATED: schema.ArticleClass is now called schema.DocumentClass') schema.DocumentClass = schema.ArticleClass } if (!schema.DocumentClass) { throw new Error('schema.DocumentClass is mandatory') } this.config.schema = schema } addEditorOption(option) { if (!option.key) { throw new Error('An option key must be defined') } if (!option.value) { throw new Error('An option value must be defined') } this.config.editorOptions[option.key] = option.value } getEditorOptions() { return this.config.editorOptions } /** Adds a node to this configuration. Later, when you use {@link Configurator#getSchema()}, this node will be added to that schema. Usually, used within a package to add its own nodes to the schema. @param {Node} NodeClass */ addNode(NodeClass) { var type = NodeClass.type if (!type) { throw new Error('A NodeClass must have a type.') } if (this.config.nodes[type]) { throw new Error('NodeClass with this type name is already registered: ' + type) } this.config.nodes[type] = NodeClass } /** Adds a converter for a conversion format. @param {string} type a conversion format type, eg. 'html', 'xml', 'json' @param {Object} converter a converter for that format. */ addConverter(type, converter) { var converters = this.config.converters[type] if (!converters) { converters = {} this.config.converters[type] = converters } if (!converter.type) { throw new Error('A converter needs an associated type.') } converters[converter.type] = converter } /** Add importer for a conversion format. @param {string} type a conversion format type. eg. 'html', 'xml' @param {Object} ImporterClass an importer for the conversion format. */ addImporter(type, ImporterClass) { this.config.importers[type] = ImporterClass } /** Add exporter for a conversion format. @param {string} type a conversion format type. eg. 'html', 'xml' @param {Object} ExporterClass an exporter for the conversion format. */ addExporter(type, ExporterClass) { this.config.exporters[type] = ExporterClass } /** Add a component for a node type. Components ({@link Component}) are the ui representation of a node for rendering and manipulation. This is usually used within a package to add representations for nodes added by that package. A component can be added once per nodeType. If you provide two components for the same node type, Substance can't figure out which one to use. @param {String} nodeType the type attribute of the node for which this component is to be used. @param {Class} ComponentClass A subclass of {@link Component} for nodes of nodeType. */ addComponent(nodeType, ComponentClass, force) { if (!force && this.config.components[nodeType]) { throw new Error(nodeType+' already registered') } if (!ComponentClass) { throw new Error('Provided nil for component '+nodeType) } if (!ComponentClass.prototype._isComponent) { throw new Error('ComponentClass must be a subclass of ui/Component.') } this.config.components[nodeType] = ComponentClass } addCommand(name, CommandClass, options) { if (!isString(name)) { throw new Error("Expecting 'name' to be a String") } if (!CommandClass) { throw new Error('Provided nil for command '+name) } if (!CommandClass.prototype._isCommand) { throw new Error("Expecting 'CommandClass' to be of type ui/Command.") } this.config.commands[name] = { name: name, CommandClass: CommandClass, options: options || {} } // Register commandGroup entry let commandGroup = options.commandGroup if (!this.config.commandGroups[commandGroup]) { this.config.commandGroups[commandGroup] = [] } this.config.commandGroups[commandGroup].push(name) } addTool(name, ToolClass) { if (!isString(name)) { throw new Error("Expecting 'name' to be a String") } if (!ToolClass) { throw new Error('Provided nil for tool '+name) } if (!ToolClass || !ToolClass.prototype._isTool) { throw new Error("Expecting 'ToolClass' to be of type ui/Tool. name:", name) } this.config.tools[name] = ToolClass } getTools() { return this.config.tools } addToolPanel(name, spec) { this.config.toolPanels[name] = spec } getToolPanel(name) { return this.config.toolPanels[name] } addManager(name, ManagerClass) { this.config.managers[name] = ManagerClass } getManagers() { return this.config.managers } /** Adds an icon to the configuration which can be later retrieved via the iconProvider. @param {string} iconName name or key for retrieving the icon @param {Object} options your custom method of representing the icon as a JSON object. Enables plugging in your own IconProvider. */ addIcon(iconName, options) { var iconConfig = this.config.icons[iconName] if (!iconConfig) { iconConfig = {} this.config.icons[iconName] = iconConfig } Object.assign(iconConfig, options) } /** Define a new label Label is either a string or a hash with translations. If string is provided 'en' is used as the language. @param {String} labelName name of label. @param {String} label label. @example ``` // Using english only. config.addLabel('paragraph.content', 'Paragraph') // Using multiple languages config.addLabel('superscript', { en: 'Superscript', de: 'Hochgestellt' }) . . // Usage within other code let labels = this.context.labelProvider $$('span').append(labels.getLabel('superscript')) ``` */ addLabel(labelName, label) { if (isString(label)) { if(!this.config.labels['en']) { this.config.labels['en'] = {} } this.config.labels['en'][labelName] = label } else { forEach(label, function(label, lang) { if (!this.config.labels[lang]) { this.config.labels[lang] = {} } this.config.labels[lang][labelName] = label }.bind(this)) } } /** Replaces the seed function for this configuration. Use a seed function to create the empty state for your document. This should be used only once per configuration. You shouldn't call this within package config methods. You can use {@link Configurator#getSeed} method to get this seed and apply it on your document {@link Document} class. @param {function} seed A transaction function that creates the seed document from an empty document. @example ```js var seedFn = function(tx) { var body = tx.get('body') tx.create({ id: 'p1', type: 'paragraph', content: 'This is your new paragraph!' }) body.show('p1') } config.addSeed(seedFn) ``` */ addSeed(seed) { this.config.seed = seed } /** Adds an editing behavior to this configuration. {@link EditingBehavior} for more. @param {EditingBehavior} editingBehavior. */ addEditingBehavior(editingBehavior) { this.config.editingBehaviors.push(editingBehavior) } addMacro(macro) { this.config.macros.push(macro) } addDragAndDrop(DragAndDropHandlerClass) { // we deprecated this after it became more clear what // we actually needed to solve console.warn('DEPRECATED: Use addDropHandler() instead') if (!DragAndDropHandlerClass.prototype._isDragAndDropHandler) { throw new Error('Only instances of DragAndDropHandler are allowed.') } this.addDropHandler(new DragAndDropHandlerClass()) } addDropHandler(dropHandler) { // legacy if (dropHandler._isDragAndDropHandler) { dropHandler.type = dropHandler.type || 'drop-asset' } this.config.dropHandlers.push(dropHandler) } addKeyboardShortcut(combo, spec) { let entry = { key: combo, spec: spec } this.config.keyboardShortcuts.push(entry) } addFileProxy(FileProxyClass) { this.config.fileProxies.push(FileProxyClass) } getFileAdapters() { return this.config.fileProxies.slice(0) } /** Configure this instance of configuration for provided package. @param {Object} pkg Object should contain a `configure` method that takes a Configurator instance as the first method. @param {Object} options Additional options to pass to the package.`configure` method @return {configurator} returns the configurator instance to make it easy to chain calls to import. */ import(pkg, options) { pkg.configure(this, options || {}) return this } // Config Interpreter APIs // ------------------------ getConfig() { return this.config } getStyles() { return this.config.styles } getSchema() { if (!this.schema) { this.schema = new DocumentSchema(this.config.schema) this.schema.addNodes(this.config.nodes) } return this.schema } getDocumentClass() { return this.config.schema.DocumentClass } createArticle(seed) { const schema = this.getSchema() const DocumentClass = schema.getDocumentClass() let doc = new DocumentClass(schema) if (seed) { seed(doc) } return doc } createImporter(type, context, options = {}) { var ImporterClass = this.config.importers[type] var config = Object.assign({ schema: this.getSchema(), converters: this.getConverterRegistry().get(type).values(), }, options) return new ImporterClass(config, context) } createExporter(type, context, options = {}) { var ExporterClass = this.config.exporters[type] var config = Object.assign({ schema: this.getSchema(), converters: this.getConverterRegistry().get(type).values() }, options) return new ExporterClass(config, context) } getCommandGroups() { return this.config.commandGroups } getComponentRegistry() { var componentRegistry = new ComponentRegistry() forEach(this.config.components, function(ComponentClass, name) { componentRegistry.add(name, ComponentClass) }) return componentRegistry } getCommands() { return map(this.config.commands, function(item, name) { return new item.CommandClass(Object.assign({name: name}, item.options)) }) } getSurfaceCommandNames() { var commands = this.getCommands() var commandNames = commands.map(function(C) { return C.name }) return commandNames } /* A converter registry is a registry by file type and then by node type `configurator.getConverterRegistry().get('html').get('paragraph')` provides a HTML converter for Paragraphs. */ getConverterRegistry() { if (!this.converterRegistry) { var converterRegistry = new Registry() forEach(this.config.converters, function(converters, name) { converterRegistry.add(name, new Registry(converters)) }) this.converterRegistry = converterRegistry } return this.converterRegistry } getDropHandlers() { return this.config.dropHandlers.slice(0) } getSeed() { return this.config.seed } getIconProvider() { return new FontAwesomeIconProvider(this.config.icons) } getLabelProvider() { return new LabelProvider(this.config.labels) } getEditingBehavior() { var editingBehavior = new EditingBehavior() this.config.editingBehaviors.forEach(function(behavior) { behavior.register(editingBehavior) }) return editingBehavior } getMacros() { return this.config.macros } getKeyboardShortcuts() { return this.config.keyboardShortcuts } getFindAndReplaceConfig() { return this.config.findAndReplace } setFindAndReplaceConfig(config) { this.config.findAndReplace = config } /* Allows lookup of a keyboard shortcut by command name */ getKeyboardShortcutsByCommand() { let keyboardShortcuts = {} this.config.keyboardShortcuts.forEach((entry) => { if (entry.spec.command) { let shortcut = entry.key.toUpperCase() if (platform.isMac) { shortcut = shortcut.replace(/CommandOrControl/i, '⌘') shortcut = shortcut.replace(/Ctrl/i, '^') shortcut = shortcut.replace(/Alt/i, '⌥') shortcut = shortcut.replace(/\+/g, '') } else { shortcut = shortcut.replace(/CommandOrControl/i, 'Ctrl') } keyboardShortcuts[entry.spec.command] = shortcut } }) return keyboardShortcuts } setDefaultLanguage(lang) { this.config.lang = lang } getDefaultLanguage() { return this.config.lang || 'en_US' } /* This is used for DependencyInjection of core implementations */ setCommandManagerClass(CommandManagerClass) { this.config.CommandManagerClass = CommandManagerClass } getCommandManagerClass() { return this.config.CommandManagerClass || DefaultCommandManager } setDragManagerClass(DragManagerClass) { this.config.DragManagerClass = DragManagerClass } getDragManagerClass() { return this.config.DragManagerClass || DefaultDragManager } setFileManagerClass(FileManagerClass) { this.config.FileManagerClass = FileManagerClass } getFileManagerClass() { return this.config.FileManagerClass || DefaultFileManager } setGlobalEventHandlerClass(GlobalEventHandlerClass) { this.config.GlobalEventHandlerClass = GlobalEventHandlerClass } getGlobalEventHandlerClass() { return this.config.GlobalEventHandlerClass || DefaultGlobalEventHandler } setKeyboardManagerClass(KeyboardManagerClass) { this.config.KeyboardManagerClass = KeyboardManagerClass } getKeyboardManagerClass() { return this.config.KeyboardManagerClass || DefaultKeyboardManager } setMacroManagerClass(MacroManagerClass) { this.config.MacroManagerClass = MacroManagerClass } getMacroManagerClass() { return this.config.MacroManagerClass || DefaultMacroManager } setMarkersManagerClass(MarkersManagerClass) { this.config.MarkersManagerClass = MarkersManagerClass } getMarkersManagerClass() { return this.config.MarkersManagerClass || DefaultMarkersManager } setSurfaceManagerClass(SurfaceManagerClass) { this.config.SurfaceManagerClass = SurfaceManagerClass } getSurfaceManagerClass() { return this.config.SurfaceManagerClass || DefaultSurfaceManager } setSaveHandlerClass(SaveHandlerClass) { this.config.SaveHandlerClass = SaveHandlerClass } getSaveHandler() { let SaveHandler = this.config.SaveHandlerClass || DefaultSaveHandler return new SaveHandler() } } export default Configurator