mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-03 12:12:39 +02:00
add start of image example
This commit is contained in:
@@ -8,9 +8,9 @@ Slate is like a pluggable implementation of `contenteditable`, built with React
|
||||
|
||||
- [Principles](#principles)
|
||||
- [Examples](#examples)
|
||||
- [Plugins](#plugins)
|
||||
- Plugins
|
||||
- [Documentation](#documentation)
|
||||
- [Contributing](#contributing)
|
||||
- Contributing
|
||||
|
||||
_Slate is currently in **beta**, while work is being done on: cross-browser support, atomic node support, and collaboration support. It's useable now, but you might need to pull request one or two fixes for your use case._
|
||||
|
||||
@@ -52,6 +52,7 @@ If you're using Slate for the first time, check out the [Getting Started](docs/g
|
||||
- [The Document Model](docs/getting-started.md#the-document-model)
|
||||
- Selections
|
||||
- [**API Reference**](docs/reference.md)
|
||||
- Plugins
|
||||
- Components
|
||||
- Editor
|
||||
- Models
|
||||
|
172
examples/images/index.js
Normal file
172
examples/images/index.js
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
import Editor, { Mark, Raw } from '../..'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import state from './state.json'
|
||||
import { Map } from 'immutable'
|
||||
|
||||
/**
|
||||
* The images example.
|
||||
*
|
||||
* @type {Component} Images
|
||||
*/
|
||||
|
||||
class Images extends React.Component {
|
||||
|
||||
state = {
|
||||
state: Raw.deserialize(state)
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert an image with `src` at the current selection.
|
||||
*
|
||||
* @param {String} src
|
||||
*/
|
||||
|
||||
insertImage(src) {
|
||||
let { state } = this.state
|
||||
|
||||
if (state.isExpanded) {
|
||||
state = state
|
||||
.transform()
|
||||
.delete()
|
||||
.apply()
|
||||
}
|
||||
|
||||
const { anchorBlock, selection } = state
|
||||
|
||||
if (anchorBlock.text == '') {
|
||||
state = state
|
||||
.transform()
|
||||
.setBlock('image', { src })
|
||||
.apply()
|
||||
}
|
||||
|
||||
else if (selection.isAtEndOf(anchorBlock)) {
|
||||
state = state
|
||||
.transform()
|
||||
.splitBlock()
|
||||
.setBlock('image', { src })
|
||||
.apply()
|
||||
}
|
||||
|
||||
else if (selection.isAtStartOf(anchorBlock)) {
|
||||
state = state
|
||||
.transform()
|
||||
.splitBlock()
|
||||
.moveToStartOfPreviousBlock()
|
||||
.setBlock('image', { src })
|
||||
.apply()
|
||||
}
|
||||
|
||||
else {
|
||||
state = state
|
||||
.transform()
|
||||
.splitBlock()
|
||||
.splitBlock()
|
||||
.moveToStartOfPreviousBlock()
|
||||
.setBlock('image', { src })
|
||||
.apply()
|
||||
}
|
||||
|
||||
this.setState({ state })
|
||||
}
|
||||
|
||||
/**
|
||||
* On clicking the image button, prompt for an image and insert it.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
|
||||
onClickImage(e) {
|
||||
e.preventDefault()
|
||||
const src = window.prompt('Enter the URL of the image:')
|
||||
if (!src) return
|
||||
this.insertImage(src)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app.
|
||||
*
|
||||
* @return {Element} element
|
||||
*/
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderToolbar()}
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the toolbar.
|
||||
*
|
||||
* @return {Element} element
|
||||
*/
|
||||
|
||||
renderToolbar() {
|
||||
return (
|
||||
<div className="menu toolbar-menu">
|
||||
<span className="button" onMouseDown={e => this.onClickImage(e)}>
|
||||
<span className="material-icons">image</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
* @return {Element} element
|
||||
*/
|
||||
|
||||
renderEditor() {
|
||||
return (
|
||||
<div className="editor">
|
||||
<Editor
|
||||
state={this.state.state}
|
||||
renderNode={node => this.renderNode(node)}
|
||||
onChange={(state) => {
|
||||
console.groupCollapsed('Change!')
|
||||
console.log('Document:', state.document.toJS())
|
||||
console.log('Selection:', state.selection.toJS())
|
||||
console.log('Content:', Raw.serialize(state))
|
||||
console.groupEnd()
|
||||
this.setState({ state })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render our custom `node`.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Element} element
|
||||
*/
|
||||
|
||||
renderNode(node) {
|
||||
switch (node.type) {
|
||||
case 'image': {
|
||||
return (props) => {
|
||||
const { data } = props.node
|
||||
const src = data.get('src')
|
||||
return <img src={src} />
|
||||
}
|
||||
}
|
||||
case 'paragraph': {
|
||||
return (props) => <p>{props.children}</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default Images
|
49
examples/images/state.json
Normal file
49
examples/images/state.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "In addition to nodes that contain editable text, you can also create other types of nodes, like images or videos."
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "image",
|
||||
"data": {
|
||||
"src": "https://img.washingtonpost.com/wp-apps/imrs.php?src=https://img.washingtonpost.com/news/speaking-of-science/wp-content/uploads/sites/36/2015/10/as12-49-7278-1024x1024.jpg&w=1484"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"kind": "text",
|
||||
"ranges": [
|
||||
{
|
||||
"text": "This example shows images in action. It features two ways to add images. You can either add an image via the toolbar icon above, or if you want in on a little secret, copy an image URL to your keyboard and paste it anywhere in the editor!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,11 +1,13 @@
|
||||
|
||||
html {
|
||||
background: #eee;
|
||||
padding: 20px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
line-height: 1.4;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 40em;
|
||||
max-width: 42em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -13,6 +15,10 @@ p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid #ddd;
|
||||
margin-left: 0;
|
||||
@@ -68,7 +74,7 @@ td {
|
||||
|
||||
.example {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.editor > * > * + * {
|
||||
@@ -93,10 +99,10 @@ td {
|
||||
}
|
||||
|
||||
.toolbar-menu {
|
||||
padding: 1px 0 9px 8px;
|
||||
margin: 0 -10px;
|
||||
padding: 1px 0 17px 18px;
|
||||
margin: 0 -20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hover-menu {
|
||||
@@ -105,11 +111,11 @@ td {
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
margin-top: -3px;
|
||||
margin-top: -6px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 1s;
|
||||
transition: opacity .75s;
|
||||
}
|
||||
|
||||
.hover-menu .button {
|
||||
|
@@ -3,6 +3,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Examples | Editor</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,400i,700,700i&subset=latin-ext" >
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -9,6 +9,7 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
|
||||
|
||||
import AutoMarkdown from './auto-markdown'
|
||||
import HoveringMenu from './hovering-menu'
|
||||
import Images from './images'
|
||||
import Links from './links'
|
||||
import PlainText from './plain-text'
|
||||
import RichText from './rich-text'
|
||||
@@ -51,6 +52,7 @@ class App extends React.Component {
|
||||
<Link className="tab" activeClassName="active" to="auto-markdown">Auto-markdown</Link>
|
||||
<Link className="tab" activeClassName="active" to="hovering-menu">Hovering Menu</Link>
|
||||
<Link className="tab" activeClassName="active" to="links">Links</Link>
|
||||
<Link className="tab" activeClassName="active" to="images">Images</Link>
|
||||
<Link className="tab" activeClassName="active" to="tables">Tables</Link>
|
||||
</div>
|
||||
)
|
||||
@@ -84,6 +86,7 @@ const router = (
|
||||
<IndexRedirect to="rich-text" />
|
||||
<Route path="auto-markdown" component={AutoMarkdown} />
|
||||
<Route path="hovering-menu" component={HoveringMenu} />
|
||||
<Route path="images" component={Images} />
|
||||
<Route path="links" component={Links} />
|
||||
<Route path="plain-text" component={PlainText} />
|
||||
<Route path="rich-text" component={RichText} />
|
||||
|
@@ -617,6 +617,31 @@ const Node = {
|
||||
.last()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the block node before a descendant text node by `key`.
|
||||
*
|
||||
* @param {String or Node} key
|
||||
* @return {Node or Null} node
|
||||
*/
|
||||
|
||||
getPreviousBlock(key) {
|
||||
key = normalizeKey(key)
|
||||
const child = this.getDescendant(key)
|
||||
let first
|
||||
|
||||
if (child.kind == 'block') {
|
||||
first = child.getTextNodes().first()
|
||||
} else {
|
||||
const block = this.getClosestBlock(key)
|
||||
first = block.getTextNodes().first()
|
||||
}
|
||||
|
||||
const previous = this.getPreviousText(first)
|
||||
if (!previous) return null
|
||||
|
||||
return this.getClosestBlock(previous)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the descendent text node at an `offset`.
|
||||
*
|
||||
|
@@ -540,6 +540,27 @@ class State extends Record(DEFAULTS) {
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selection to the start of the previous block.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
moveToStartOfPreviousBlock() {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
let blocks = document.getBlocksAtRange(selection)
|
||||
let block = blocks.first()
|
||||
if (!block) return state
|
||||
|
||||
let previous = document.getPreviousBlock(block)
|
||||
if (!previous) return state
|
||||
|
||||
selection = selection.moveToStartOf(previous)
|
||||
state = state.merge({ selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block nodes in the current selection to `type`.
|
||||
*
|
||||
|
@@ -75,6 +75,7 @@ const STATE_TRANSFORMS = [
|
||||
'insertFragment',
|
||||
'insertText',
|
||||
'mark',
|
||||
'moveToStartOfPreviousBlock',
|
||||
'setBlock',
|
||||
'setInline',
|
||||
'splitBlock',
|
||||
|
Reference in New Issue
Block a user