Giter Site home page Giter Site logo

substance / substance Goto Github PK

View Code? Open in Web Editor NEW
2.7K 70.0 122.0 9.07 MB

A JavaScript library for web-based content editing.

Home Page: https://substance.io

License: MIT License

JavaScript 89.19% CSS 0.06% HTML 10.76%
text-editor editing html xml annotations publishing javascript

substance's Introduction

Substance.js Build Status

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform Substance.

Development

Install the dev dependencies.

npm install

Run the dev server.

npm start

Navigate to http://localhost:4001/test for the running the browser test suite. The test suite is rebuilt as you make changes to the source files.

To run the test-suite headless.

$ npm test

substance's People

Contributors

asmecher avatar dannelundqvist avatar dmukhg avatar integral avatar jure avatar kilburn avatar nokome avatar obuchtala avatar oliver7654 avatar philippamarkovics avatar podviaznikov avatar radarhere avatar rquast avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

substance's Issues

Concept for tools / keybindings triggering the same action

Would like to look into this tomorrow. It's related to #23. Would be nice to have some generic concept for keybindings and somehow make this work nicely in combination with tools. E.g. cmd+b and clicking the strong tool should trigger the same action.

One thing we discussed was breaking out the AnnotationTool implementation into a ToggleAnnotation transformation. This must provide static functions for canCreate(sel), canExpand etc. that the tool implementation can use to update the button states. In performAction we just execute the transformation using a surface transaction. Parameter being the selection and the annotationType to be toggled.

With that interface we could also introduce custom keybindings that call the same function. Something like this:

var toggleAnnotation = require('substance/document/transformations/toggle_annotation');

var ToggleStrong = KeyBinding.extend({
  handles: ['command+b', 'ctrl+b'],
  performAction: function() {
    var surface = this.context.surfaceManager.getSurface();
    surface.transaction(function(tx, args) {
      args.annotationType = 'strong';
      toggleAnnotation(tx, args);
    });
  }
});

What do you think? We will need some orchestration to announce the desired keybindings to the app. For the Writer/Editor components this could just be passed like this:

var MyEditor = Editor.extend({
  config: {
    article: Article,
    toolbar: Toolbar,
    keybindings: [ToggleStrong, ToggleLink],
    components: {
      "paragraph": require('substance/ui/nodes/paragraph_component'),
      "link": require('substance/ui/nodes/link_component')
    }
  }
});

Could we still use the Substance Keyboarder to manage things? Need your input here generally before I implement this.

Custom rendering of selections

  • Surface DDAU: TextProperties shouldn't pull annotations from surface, but instead get them via props from above. Surface is just the better place to know about highlights and selections.
  • Live rendering of selections and cursor: maybe primitive rerender is quick enough for a good enough (flickering?)
  • Scroll to current selection: when we render the selection ourselves, we should be able to scroll to it.

Allow useing OO.makeExtensible with OO.inherit

I'd like to add an extra entry command to defaultKeyProps for the Tool class, so I can:

var StrongTool = AnnotationTool.extend({
  name: 'strong',
  command: 'toggleStrong'
});

module.exports = StrongTool;

However it seems that OO.makeExtensible does not work when used in conjunction with OO.inherit. Any ideas how I should approach this?

OO.makeExtensible(Tool, { "name": true, "displayName": true, "command": true });
OO.inherit(Tool, Component);

Add Example/API for Real Time Collaboration

Even though collaborative document editing is listed as a possibility with the editor (because of it's OT changes nature), there don't seem to be any examples/tutorials/api docs that describe how to do it. I would expect the DocumentChange class to be serializable (e.g. have toJSON and fromJSON methods) so that changes can be transmitted over the wire and instantiated in another users editor. Those methods would also help with DocumentChange persistence in any db type (RDBMS or Document).

Add convenience to add listeners for node changes.

ATM, we use something like

doc.getEventProxy('path').add([node.id, 'title'], this, this.onChange)

It would be nicer to be able just to do:

node.connect(this, {
  'title:changed', this.onTitleChanged
});

or for any property change:

node.connect(this, {
  'node:changed': this.onChange
});

To disconnect:

node.disconnect(this);

Find a better solution for global.$

ATM we rely on jquery being available globally.

In the browser we need either include jquery via script tag or do this

var $ = window.$ = require('jquery');

On the server we use cheerio, and register it the same way.

It would be better to have a index file serving jQuery or cheerio.

var $ = require('substance/jqueryish');

Find a better name.

Problem with refs

There seems to be some bug with the ref mechanism that hit me when bringing highlights back for the Scrollbar.

From scrollbar.js render:

      var highlightEls = this.props.highlights.map(function(h) {
        return $$('div').ref(h)
          .addClass('highlight');
      });

E.g. when in the first run ref is set to topic_annotation_1 and in a next run topic_annotaton_2 is in the highlights the this.refs['topic_annotatoin_2'] is not available.

You can reproduce using the substance/demos#topic-writer branch. Then:

  • Put cursor in the first topicref (highlight is correct)
  • Put cursor in second topicref (the ref is not available, see warning on the console)

@oliver---- could you investigate?

Shipping modular Substance distributions (or not)

I was thinking of what would be the best way to deliver a dist for the HTMLEditor component. It would be nice to just have a single minified js file that one can include in a client-side app. However keeping customisability alive we would also need to expose Substance.Component, so it would be more a SubstanceWithHtmlEditor dist. Usage:

// window.Substance is available.
var Component = Substance.Component;
var $$ = Component.$$;

var Toolbar = Component.extend(...);
var htmlEditor = $$(Substance.HtmlEditor, {
  toolbar: Toolbar,
  content: "<p>some text</p>"
});
Component.mount(htmlEditor, $('#editor_container'));

I'm not sure if this is a good way to be honest. For instance if you want to use some other Substance components (e.g. writer) then you would need to 2 dists.. duplicating a lot of code.

Maybe we should use the following strategy, shipping one preconfigured HtmlEditor with standard toolbar to be used by the kids that just want to embed a widget, and for all others we require a browserify workflow.

Usage of the simplest Substance Editor widget would then look like:

Substance.HtmlEditor.init('#editor_container');

Thoughts?

[Question] Implementation of OT Changes

Sorry for spamming with issues these days, been starting to play with the current version but at some stage would love some clarifications on the roadmap for future versions.

I am working on a personal wiki system, with a touch of Etherpad in which each page persists automatically over time. In the ideal case, in setting up of a doc, the current text and the revision history are populated from server-side, and one can go back to some point in time and edit that version. If the edited version is not the latest one, a new branch is made. I am also planning to add a tree visualizer for all the branches.

With reference to the current Document class implementation, it makes use of two arrays (done and undone) for each transformations for Undo/Redo. If I am to implement that particular feature here, each transformation should be assigned a unique identifier so that when I do an Undo sequence I would know exactly which revision should I base my following transactions on.

I am not sure if I am trying to reinvent the wheel for this kind of mechanisms here, nor if anything is already covered in our roadmap here. Would appreciate any pointers to the OT mechanism here. Thanks.

Component: didUpdate / willUpdate lifecycle hooks

I wonder if we should introduce a generic lifecycle hook analog to React's componentDidUpdate. Currently we expose willUpdateState and didUpdateState. However, that way we need to listen explicitly to either didUpdateState and didReceive props. I like that it's more explicit, but in many cases you just want to do something whenever the component has updated, and you don't care whether a state update or props update was happening.

What I found convenient in React was that componentWillUpdate gives you both, nextProps and nextState. We could do the same.

@oliver---- thoughts?

Refactor tools

To reduce complexity we decided to implement Tools (e.g. StrongTool, LinkTool) as Substance Components.

Currently we have an indirection where tools have a model (for data manipulation) and a view (visual representation). Since tool state is maintained in the model the component/view needs to listen to those state changes and rerender accordingly.

Implementation of new tools will get easier implementing them as components right away and using inheritance to reuse implementations.j

We have different categories of tools, which should also be considered in this refacor.

  • SurfaceTool (a tool that is closely coupled with the cursor/selection, e.g. LinkTool)
  • DocumentTool (not related to current selection, e.g. UndoTOol)

We will get a class hierarchy like this:

  • Tool
    • SurfaceTool
      • AnnotationTool
        • StrongTool
    • DocumentTool
      • UndoTool

HtmlEditor: Allow editing of link title

I disabled editing of link.title attributes as it makes for a much nicer user experience during editing. E.g. the user can just press enter to confirm an url change.

image

People will in 90% of the cases not add a title to the link anyway. However we should allow to change that information somehow, maybe be adding a button in the edit link prompt to toggle between title and url editing.

Make insert transformation preserve annotations

Current behavior for inserting 'a' in different selection scenarios:

First char of annotation touched:
image

image

Strict overlap:
image

image

Desired behaviour: the inserted character should preserve all annotations.

Allow returning VirtualComponent in render

render: function() {
  return $$(ChildComponent);
}

Does not work currently. We expect the element returned by render always to be an HTML Element. This would an important feature so we can save some markup. It's like delegating the output to the child component.

React.js versus custom component.js?

In the interest of incorporating this library into some of my projects that are implement within react-driven interfaces, I'm looking for more documentation or help understanding the decision to drop react from your code in favor of what appears to be a close re-engineering of react components (in component.js). I'm also looking for any clear documentation on the relationship between the "legacy" (react based) version of the library and this one, as well as an idea as to whether the legacy code will now be officially left dead or will continue to see work in parallel.

For applications that are already built within react, the legacy library seems more appealing and supports the excellent example of Lens Writer which accomplishes much of what I'm looking for. It's a very hard sell, at least at first glance, to commit to the limitations of a pared-down custom implementation of react-like components (using all the same lifecycle hooks and concepts, from what I can tell) that will then need constant work to match pace with react's updates over time and that won't integrate with everything react offers above and below in an app (ie., existing (child)context already passed down the tree that I'd like to make available to the editor, incorporation of other mature UI element libraries in react for building custom widgets, etc).

The code does imply that you dropped react in favor of synchronous rendering... I wondered if the recent split (in 0.14) between react (as a component library) and react-dom (as merely one rendering implementation that is now swappable for others) could alleviate some of that overhead if you merely wished to replace the rendering of react-dom but keep react's components as your base. But obviously I haven't dug deep enough into the code here to see how this all shakes out.

Mainly just seeking information on dropping react and an idea of whether the legacy project will soon be effectively dead -- I might have merely overlooked where I can find that.

Component: Supply HTML string

There's currently no way to append HTML directly in a component's render method. We should have a .html() method, like in jQuery:

var el = $$('div').addClass('html-container').html('<b>foo</b>');

Not sure if this should work only for HTML constructors or also for custom components. This could potentially conflict with a component's render method. E.g. when you do

var el = $$(MyComponent).html('<b>foo</b>');

Will MyComponent.render be called then? And then overridden? It seems like a bad idea to allow that, but on the other hand the Component API should be consistent.

Writer: Make Panel.getPanelOffsetForElement more robust

Currently we only do $(el).position().top. However if we assign position: absolute or position: relative to any element in the container, the offset calculation of all children of this element will get wrong.

We solved this for the Archivist project by walking up the tree and adding $(el).position().top to the offset for each element that is positioned absolute or relative.

Let's review the implementation and bring this improvement into substance#master.

  this.getPanelOffsetForElement = function(el) {
    // initial offset
    var offset = $(el).position().top;

    // Now look at the parents
    function addParentOffset(el) {
      var parentEl = el.parentNode;

      // Reached the panel or the document body. We are done.
      if ($(el).hasClass('panel-content-inner') || !parentEl) return;

      // Found positioned element (calculate offset!)
      if ($(el).css('position') === 'absolute' || $(el).css('position') === 'relative') {
        offset += $(el).position().top;
      }
      addParentOffset(parentEl);
    }

    addParentOffset(el.parentNode);
    return offset;
  };

Htmleditor: dom should be loaded

Looks like dom should be loaded when you are attaching html-editor, otherwise toolbar will be not activated. It's not a bug I think... But you should wright about this.

ListItem serialization

This should conform to the minimal HTML needed, as with other elements.

Currently we get:

<li class="content-node list-item" data-id="li_f5a85192267a42bbeb24d7a871cb38b4">Point B</li>

Desired:

<li id="li_f5a85192267a42bbeb24d7a871cb38b4">Point B</li>

key / ref mechanism does not work on root element

This fails

QUnit.test("Key/ref tests", function(assert) {
  var ClickableComponent = TestComponent.extend({
    render: function() {
      return $$('a').attr('href', '#').key('link').append('hello');
    }
  });

  var comp = Component.mount($$(ClickableComponent), $('#qunit-fixture'));
  assert.ok(comp.refs.link, 'Should have a ref to link');
});

Could we do something about it, for consistency? I understand that i could reach the root element using comp.$el, but somehow it's strange i can not use it.

Read-only components

Would it be feasible to create a substance component, e.g. string of text that is read-only, but still behaves like actual text (so it can be inserted in the middle of a paragraph, and act like any other string there - except it cannot be changed)?

Reorganise sass styles

We need to remove site-specific layout bits from base.css and provide a separate module. The reason is that people may want to use the Substance HTML editor but within their own site style. So they just need styles for dropdown etc. but not our font etc. and base styles for paragraphs etc.

This may take a bit of time to get right, and it's not so relevant during the HTML Editor beta phases. But it will become very import when people start embedding Substances on their sites.

Provide val() val('foo') for Element Components.

Let's us do:

function login() {
  var username = this.refs.name.val();
  var passwd = this.refs.passwd.val();
}

Instead of:

function login() {
  var username = this.refs.name.$el.val();
  var passwd = this.refs.passwd.$el.val();
}

key vs. ref and component recycling

I found an argument why we should probably support both key and ref.

A key must only be unique only within the direct parent context, while a ref must be unique within the whole owner component. Thus this is perfectly valid:

  render: function() {
    return $$('div').append([
      $$('div').key('a').append([
        $$('div').key('a'),
        $$('div').key('b')
      ]),
      $$('div').key('b').append([
        ..
      ])
    ]);
  }

But this is not:

  render: function() {
    return $$('div').append([
      $$('div').ref('a').append([
        $$('div').ref('a'),
        $$('div').ref('b')
      ]),
      $$('div').ref('b').append([

      ])
    ]);
  }

Usually we would use a ref for static components, that we know they will be there (e.g. contentPanel, scrollbar, etc.) and keys for comps that are dynamically generated (e.g. for list of content nodes).

I'd like to activate reusing of components whenever there's a key present. So we don't have to add another persistent() marker. That said, I was wondering if the default implementation of shouldRerender should just return true... then we don't have any edge cases where something does not get updated and no costly deep checking by default. whenever you want to stop the hard rerendering (which should in many many cases also be very fast) you implement shouldRerender (which is usually very simple and then we have things totally explicit).

In our case for components that are updated using events we could just do return false in the customer shouldRerender, so this component would just say.. 'I manage myself'. In our case ContentPanel would be such an example.

Question: Is Component.prototype.mount in use?

Is Component.prototype.mount in use? It seems not to be called anywhere.

  this.mount = function($el) {
    this._render(this.render());
    $el.append(this.$el);
    // trigger didMount automatically if the given element is already in the DOM
    if (_isInDocument($el[0])) {
      this.triggerDidMount();
    }
    return this;
  };

Also I don't understand why we check for for _isInDocument and then call triggerDidMount. Shouldn't it be the other way: if not yet mounted we call triggerDidMount (as it's the first time).

Component: Improve check whether triggerDidMount should be called or not

We just fixed an edge case related to component mounting.

e71a511

However I introduced a new mounted instance variable, which holds the same boolean as a call to _isInDocument. I think we should have one way to do this, and make it a clear API.

How about introducing a static method:

Component.isMounted = function(comp) {
}

We could do the check of _isInDocument there but later also optimize by using a map internally to determine faster if a component has been mounted or not. Related: Should we consider introducing a unique id for each component?

Introduce keybindings

Currently we define all keybindings hard-coded within Surface. We should come to some keyMap approach where we assign shortcuts with the commandName that should be triggered.

Write more Component tests

Particularly for the following cases.

  • Various usage scenarios of .html(). Think of how to deal with wrong usage. E.g. calling .html() in combination with .append()
  • Test various scenarios of DOM diffing (preserving child when ref is given)
  • Test various scenarios of shouldComponentUpdate
  • Test Component.mount (passing either a VirtualComponent, or a real Component instance)
  • Test didRender hook
  • Test didMount and willUnmount in combination with didRender

Make scroll follow the cursor

This is really needed for a serious editing experience. I will work on a scrollToCursor/Selection implementation and then we connect it with the Surface.

Text cursor movement by tap fails on iPad

As guessed in #28 Substance isn't quite ready for mobile yet.

Current Substance demos render on iPad and it is possible to type, but only at the original text cursor position. You can tap somewhere else in the text and the text cursor will move, but you still type at the original location.

On iPhone the demos have issues with the small screen and it is hard to get to typing.

On an Android 4.4 phone with 1020p screen, both portrait and landscape, it is only the credits footer that renders while the rest of the screen is grey.

Better mechanism for node highlights

We need a better mechanism for managing annotation active states (currently implemented as a volatile active property on annotations)

The current approach is pretty perfect for highlighting nodes that are displayed somewhere in a container and are already rendered in one or more places. using anno.setActive(true) we incrementally trigger class changes for each node.

However for the Scrollbar, we don’t have a component for each annotation. We need to make up some new ones based on the set of currently highlighted nodes. So we first have to ask, what is the current set of highlighted nodes, then calculate their positions (in the container the scrollbar is associated with) and render them as part of the scrollbar. This info is currently not available, so we somehow must ask the generic annotationIndex and walk through all annos to find the ones that have active set to true. This we would need to do each time somewhere anno.setActive(..) is called. Not exactly efficient.

What if we made this a Surface API and don’t play it on the document itself, since it’s more a UI thing anyway. We should probably just reset the set of active nodes in one go, that way we can trigger the scrollbar update just once, after active nodes have been set. E.g. like this. (I thought I call the concept highlight, because active is confusing (we have active tools etc.)

surface.setHighlights(['anno1', 'anno2']);

Then we should implement some convenient event proxy that can be used by components. We could still emit node:highlighted events on individual nodes, so the node components can easily subscribe to changes, or also setting the volatile highlighted property (is convenient for the render method, so we don’t have to keep things in the component state)

surface.connect(this, {
  // triggered for each node
  'node:highlighted': this.onNodeHighlighted 
  // triggered once after setHighlights has been called
  // it transports a list of node ids that have been highlighted
  // can be used by the scrollbar / content panel
  'nodes:highlighted': this.onNodesHighlighted
});

Better getDocumentMeta

Hey,

if you don't mind I will make some proposals sometimes.

Maybe I'm carping, but would be great if doc.getDocumentMeta(prop) will be alias for doc.getDocumentMeta()[prop].

Critical: Pressing cmd + b or cmd + i breaks the editing

What happens is that contenteditable kicks in and adds b and i tags. However this breaks the editor. What we should probably do short-term is intercepting all those default keyboard events and delegate to our own actions. Actually i wonder if this should be connected with the tools somehow. Because we would need the same logic canTruncate canExpand etc.

Long-term we should enable this DOM MutationObserver thing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.