Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][PROOF OF CONCEPT] Line Breaks 🤡 #780

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export default args => [
input: 'tests/index.js',
plugins: [
...commonPlugins(),
globImport(),
globImport({
format: 'import'
}),
copy({
targets: [
{ src: 'dist/mobiledoc.js', dest: 'assets/demo' },
Expand Down
70 changes: 67 additions & 3 deletions src/js/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ import Post from '../models/post'
import { Mobiledoc } from '../renderers/mobiledoc'
import { SectionParserPlugin } from '../parsers/section'
import { CardData, CardRenderHook } from '../models/card-node'
import { AtomData } from '../models/atom-node'
import { AtomData } from '../models/atoms/atom-node'
import { Option, Maybe, Dict } from '../utils/types'
import Markup from '../models/markup'
import View from '../views/view'
import Atom, { AtomPayload } from '../models/atom'
import Atom, { AtomPayload } from '../models/atoms/atom'
import Section, { isNested } from '../models/_section'
import { TextInputHandlerListener } from './text-input-handler'
import ElementAtom from 'mobiledoc-kit/models/atoms/element-atom'

// This export may later be deprecated, but re-export it from the renderer here
// for consumers that may depend on it.
Expand All @@ -76,6 +77,14 @@ export interface EditorOptions {
nodeType?: number
}

const SoftBreakAtom: AtomData = {
name: '-soft-break',
type: 'dom',
render() {
return document.createElement('br')
}
}

const defaults: EditorOptions = {
placeholder: 'Write here...',
spellcheck: true,
Expand All @@ -84,7 +93,7 @@ const defaults: EditorOptions = {
undoDepth: 5,
undoBlockTimeout: 5000, // ms for an undo event
cards: [],
atoms: [],
atoms: [SoftBreakAtom],
cardOptions: {},
unknownCardHandler: ({ env }) => {
throw new MobiledocError(`Unknown card encountered: ${env.name}`)
Expand Down Expand Up @@ -1189,6 +1198,27 @@ export default class Editor implements EditorOptions {
})
}

insertTextWithMarkup(text: string, markups: Markup[] = []) {
if (!this.hasCursor()) {
return
}
if (this.post.isBlank) {
this._insertEmptyMarkupSectionAtCursor()
}
let {
range,
range: { head: position },
} = this

this.run(postEditor => {
if (!range.isCollapsed) {
position = postEditor.deleteRange(range)
}

postEditor.insertTextWithMarkup(position, text, markups)
})
}

/**
* Inserts an atom at the current cursor position. If the editor has
* no current cursor position, nothing will be inserted. If the editor's
Expand Down Expand Up @@ -1223,6 +1253,40 @@ export default class Editor implements EditorOptions {
return atom!
}

/**
* Inserts an atom at the current cursor position. If the editor has
* no current cursor position, nothing will be inserted. If the editor's
* range is not collapsed, it will be deleted before insertion.
* @param {String} atomName
* @param {String} [atomText='']
* @param {Object} [atomPayload={}]
* @return {Atom} The inserted atom.
* @public
*/
insertElementAtom(tagName: string): Maybe<ElementAtom> {
if (!this.hasCursor()) {
return
}

if (this.post.isBlank) {
this._insertEmptyMarkupSectionAtCursor()
}

let atom: ElementAtom
let { range } = this
this.run(postEditor => {
let position = range.head

atom = postEditor.builder.createElementAtom(tagName)
if (!range.isCollapsed) {
position = postEditor.deleteRange(range)
}

postEditor.insertMarkers(position, [atom])
})
return atom!
}

/**
* Inserts a card at the section after the current cursor position. If the editor has
* no current cursor position, nothing will be inserted. If the editor's
Expand Down
5 changes: 5 additions & 0 deletions src/js/editor/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ export default class EventManager {
event.preventDefault()
break
}
case key.isShiftEnter():
event.preventDefault()
// editor.insertTextWithMarkup(' ', [editor.builder.createMarkup('br')])
editor.insertElementAtom('br')
break
case key.isEnter():
this._textInputHandler.handleNewLine()
editor.handleNewline(event)
Expand Down
14 changes: 7 additions & 7 deletions src/js/models/atom-node.ts → src/js/models/atoms/atom-node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Atom from './atom'
import assert from '../utils/assert'
import { JsonData, Dict, Maybe } from '../utils/types'
import CustomAtom from './custom-atom'
import assert from '../../utils/assert'
import { JsonData, Dict, Maybe } from '../../utils/types'

export type AtomOptions = Dict<unknown>

Expand All @@ -12,7 +12,7 @@ export interface AtomRenderOptions {
payload: JsonData
}

export type AtomRenderHook = (options: AtomRenderOptions) => Maybe<Element>
export type AtomRenderHook = (options: AtomRenderOptions) => Maybe<Element> | void

export type AtomData = {
name: string
Expand All @@ -23,14 +23,14 @@ export type AtomData = {
export default class AtomNode {
editor: any
atom: AtomData
model: Atom
model: CustomAtom
element: Element
atomOptions: AtomOptions

_teardownCallback: TeardownCallback | null = null
_rendered: Maybe<Node>

constructor(editor: any, atom: AtomData, model: Atom, element: Element, atomOptions: AtomOptions) {
constructor(editor: any, atom: AtomData, model: CustomAtom, element: Element, atomOptions: AtomOptions) {
this.editor = editor
this.atom = atom
this.model = model
Expand All @@ -46,7 +46,7 @@ export default class AtomNode {
model: { value, payload },
} = this
// cache initial render
this._rendered = this.atom.render({ options, env, value, payload })
this._rendered = this.atom.render({ options, env, value, payload }) || null
}

this._validateAndAppendRenderResult(this._rendered!)
Expand Down
6 changes: 6 additions & 0 deletions src/js/models/atoms/atom-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const enum AtomType {
CUSTOM = 1,
ELEMENT = 2
}

export default AtomType
14 changes: 14 additions & 0 deletions src/js/models/atoms/atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Type } from '../types'
import ElementAtom from './element-atom'
import CustomAtom from './custom-atom'
import { PostNode } from '../post-node-builder'

export { default as AtomType } from './atom-type'
export default CustomAtom
export { AtomPayload } from './custom-atom'

export type SomeAtom = ElementAtom | CustomAtom

export function isAtom(postNode: PostNode): postNode is SomeAtom {
return postNode.type === Type.ATOM
}
16 changes: 7 additions & 9 deletions src/js/models/atom.ts → src/js/models/atoms/custom-atom.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Type } from './types'
import Markuperable from '../utils/markuperable'
import assert from '../utils/assert'
import Markup from './markup'
import PostNodeBuilder, { PostNode } from './post-node-builder'
import { Type } from '../types'
import Markuperable from '../../utils/markuperable'
import assert from '../../utils/assert'
import Markup from '../markup'
import PostNodeBuilder from '../post-node-builder'
import AtomType from './atom-type'

const ATOM_LENGTH = 1

export type AtomPayload = {}

export default class Atom extends Markuperable {
type: Type = Type.ATOM
atomType = AtomType.CUSTOM
isAtom = true

name: string
Expand Down Expand Up @@ -95,7 +97,3 @@ export default class Atom extends Markuperable {
return [pre, post]
}
}

export function isAtom(postNode: PostNode): postNode is Atom {
return postNode.type === Type.ATOM
}
92 changes: 92 additions & 0 deletions src/js/models/atoms/element-atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Type } from '../types'
import Markuperable from '../../utils/markuperable'
import assert from '../../utils/assert'
import Markup from '../markup'
import PostNodeBuilder from '../post-node-builder'
import AtomType from './atom-type'

const ATOM_LENGTH = 1

export default class ElementAtom extends Markuperable {
type: Type = Type.ATOM
atomType = AtomType.ELEMENT

isMarker = false
isAtom = true

name: string
value: string = ''
text: string = ''

markups: Markup[]
builder!: PostNodeBuilder

constructor(tagName: string, markups: Markup[] = []) {
super()
this.name = tagName

this.markups = []
markups.forEach(m => this.addMarkup(m))
}

clone() {
let clonedMarkups = this.markups.slice()
return this.builder.createElementAtom(this.name, clonedMarkups)
}

get isBlank() {
return false
}

get length() {
return ATOM_LENGTH
}

canJoin(/* other */) {
return false
}

textUntil(/* offset */) {
return ''
}

split(offset = 0, endOffset = offset) {
let markers: Markuperable[] = []

if (endOffset === 0) {
markers.push(this.builder.createMarker('', this.markups.slice()))
}

markers.push(this.clone())

if (offset === ATOM_LENGTH) {
markers.push(this.builder.createMarker('', this.markups.slice()))
}

return markers
}

splitAtOffset(offset: number): [Markuperable, Markuperable] {
assert('Cannot split a marker at an offset > its length', offset <= this.length)

let { builder } = this
let clone = this.clone()
let blankMarker = builder.createMarker('')
let pre: Markuperable, post: Markuperable

if (offset === 0) {
;[pre, post] = [blankMarker, clone]
} else if (offset === ATOM_LENGTH) {
;[pre, post] = [clone, blankMarker]
} else {
assert(`Invalid offset given to Atom#splitAtOffset: "${offset}"`, false)
}

this.markups.forEach(markup => {
pre.addMarkup(markup)
post.addMarkup(markup)
})

return [pre, post]
}
}
1 change: 1 addition & 0 deletions src/js/models/markup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const VALID_MARKUP_TAGNAMES = [
'sub', // subscript
'sup', // superscript
'u',
'br'
].map(normalizeTagName)

export const VALID_ATTRIBUTES = ['href', 'rel']
Expand Down
15 changes: 14 additions & 1 deletion src/js/models/post-node-builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Atom, { AtomPayload } from './atom'
import Atom, { AtomPayload } from './atoms/custom-atom'
import Post from './post'
import MarkupSection from './markup-section'
import ListSection from './list-section'
Expand All @@ -19,6 +19,7 @@ import Markuperable from '../utils/markuperable'
import Section from './_section'
import { Cloneable } from './_cloneable'
import { Dict } from '../utils/types'
import ElementAtom from './atoms/element-atom'

function cacheKey(tagName: string, attributes: Dict<string>) {
return `${normalizeTagName(tagName)}-${objectToSortedKVArray(attributes).join('-')}`
Expand Down Expand Up @@ -152,6 +153,18 @@ export default class PostNodeBuilder {
return atom
}

/**
* @param {String} name
* @param {String} [value='']
* @param {Markup[]} [markups=[]]
* @return {Atom}
*/
createElementAtom(tagName: string, markups: Markup[] = []): ElementAtom {
const atom = new ElementAtom(tagName, markups)
atom.builder = this
return atom
}

/**
* @param {String} tagName
* @param {Object} attributes Key-value pairs of attributes for the markup
Expand Down
2 changes: 1 addition & 1 deletion src/js/models/render-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import assert, { unwrap } from '../utils/assert'
import RenderTree from './render-tree'
import { Option } from '../utils/types'
import CardNode from './card-node'
import AtomNode from './atom-node'
import AtomNode from './atoms/atom-node'
import Section from './_section'
import Markuperable from '../utils/markuperable'
import { PostNode } from './post-node-builder'
Expand Down
2 changes: 1 addition & 1 deletion src/js/parsers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Cloneable } from '../models/_cloneable'
import MarkupSection, { hasInferredTagName } from '../models/markup-section'
import RenderTree from '../models/render-tree'
import { isMarker } from '../models/marker'
import { isAtom } from '../models/atom'
import { isAtom } from '../models/atoms/atom'
import RenderNode from '../models/render-node'
import Markuperable from '../utils/markuperable'
import ListItem from '../models/list-item'
Expand Down
Loading