Code Monkey home page Code Monkey logo

slate-edit-table's Introduction

⚠️ This repository is archived and has moved to GitBook's fork of ianstormtaylor/slate. Previous versions are still available on NPM All the versions using GitBook's fork of slate are now published under the @gitbook NPM scope. To learn more about why we forked Slate, read our manifest

slate-edit-table

NPM version Linux Build Status

A Slate plugin to handle table edition.

Demo: gitbookio.github.io/slate-edit-table/

Install

npm install slate-edit-table

Features

  • Pressing Up/Down moves the cursor to the row above/below
  • Pressing Enter inserts a new row
  • Pressing Cmd+Enter (Ctrl+Enter on Windows/Linux) exits the table, into a new default block
  • Pressing Tab moves the cursor to next cell
  • Pressing Shift+Tab moves the cursor to previous cell

All these default features are configurable.

Copy/Paste behavior

Here are how different cases of copy-paste are handled by the plugin:

  1. Copying the content of a single cell into another cell → The content of the first cell is pasted inside the second cell
  2. Copying the content of a single cell outside the table → Just the content of the cell is pasted (not the table)
  3. Copying some content into a cell → The content is inserted inside the cell
  4. Copying multiple cells somewhere else inside the table → The copied fragment of table is patched at the given position, overwritting cells and adding rows and columns if necessary.
  5. Copying multiple cells outside the table → A new table is pasted, containing the copied cells.

Simple Usage

import EditTable from 'slate-edit-table';

const tablePlugin = EditTable(/* options */);

const plugins = [tablePlugin];

Data structure

Here is what your Slate document containing tables should look like:

<value>
    <document>
        <paragraph>Some text</paragraph>

        <table>
            <table_row>
                <table_cell>
                    <paragraph>Any block can goes into cells</paragraph>
                </table_cell>

                <table_cell>
                    <image isVoid src="image.png" />
                </table_cell>
            </table_row>

            <table_row>
                <table_cell>
                    <paragraph>Second row</paragraph>
                </table_cell>

                <table_cell>
                    <paragraph>Second row</paragraph>
                </table_cell>
            </table_row>
        </table>
    </document>
</value>

Options

Option object you can pass to the plugin.

  • [typeTable: string] — type for table
  • [typeRow: string] — type for the rows.
  • [typeCell: string] — type for the cells.
  • [typeContent: string] — default type for blocks in cells. Also used as default type for blocks created when exiting the table with Mod+Enter.

EditTable

EditTable(options: Options) => Instance

Constructs an instance of the table plugin, for the given options. You can then add this instance to the list of plugins passed to Slate.

Once you have constructed an instance of the plugin, you get access to utilities and changes through pluginInstance.utils and pluginInstance.changes.

Utils

utils.isSelectionInTable

isSelectionInTable(value: Slate.Value) => boolean

Return true if selection is inside a table cell.

utils.isSelectionOutOfTable

isSelectionOutOfTable(value: Slate.Value) => boolean

Return true if selection starts and ends both outside any table. (Notice: it is NOT the opposite value of isSelectionInTable)

utils.getPosition

getPosition(value: Slate.Value) => TablePosition

Returns the position of the cursor in a table (and all related infos).

utils.getPositionByKey

getPositionByKey(tableAncestor: Node, key: string) => TablePosition

Returns the position of a particular node in a table (and all related infos).

utils.createTable

createTable(
    columns: number,
    rows: number,
    getCellContent?: (row: number, column: number) => Node[]
): Block

Returns a table. The content can be filled with the given getCellContent generator.

utils.createRow

createRow(
    columns: number,
    getCellContent?: (column: number) => Node[]
): Block

Returns a row. The content can be filled with the given getCellContent generator.

utils.createCell

createCell(opts: Options, nodes?: Node[]): Block

Returns a cell. The content defaults to an empty typeContent block.

Changes

changes.insertTable

insertTable(change: Change, columns: ?number, rows: ?number) => Change

Insert a new empty table.

changes.insertRow

insertRow(
    opts: Options,
    change: Change,
    at?: number, // row index
    getRow?: (columns: number) => Block // Generate the row yourself
): Change

Insert a new row after the current one or at the specific index (at).

changes.insertColumn

insertColumn(
    opts: Options,
    change: Change,
    at?: number, // Column index
    getCell?: (column: number, row: number) => Block // Generate cells
): Change

Insert a new column after the current one or at the specific index (at).

changes.removeTable

removeTable(change: Change) => Change

Remove current table.

changes.removeTableByKey

removeTableByKey(change: Change, key: string) => Change

Remove the table containing the given key.

changes.removeRow

removeRow(change: Change, at: ?number) => Change

Remove current row or the one at a specific index (at).

changes.removeRowByKey

removeRowByKey(change: Change, key: string) => Change

Remove the row containing the given key.

changes.removeColumn

removeColumn(change: Change, at: ?number) => Change

Remove current column or the one at a specific index (at).

changes.removeColumnByKey

removeColumnByKey(change: Change, key: string) => Change

Remove the column containing the given key.

changes.moveSelection

moveSelection(change: Change, column: number, row: number) => Change

Move the selection to a specific position in the table.

changes.moveSelectionBy

moveSelectionBy(change: Change, column: number, row: number) => Change

Move the selection by the given amount of columns and rows.

TablePosition

An instance of TablePosition represents a position within a table (row and column). You can get your current position in a table by using plugin.utils.getPosition(value).

position.getWidth() => number

Returns the number of columns in the current table.

position.getHeight() => number

Returns the number of rows in the current table.

position.getRowIndex() => number

Returns the index of the current row in the table.

position.getColumnIndex() => number

Return the index of the current column in the table.

position.isFirstCell() => boolean

True if on first row and first column of the table

position.isLastCell() => boolean

True if on last row and last column of the table

position.isFirstRow() => boolean

True if on first row

position.isLastRow() => boolean

True if on last row

position.isFirstColumn() => boolean

True if on first column

position.isLastColumn() => boolean

True if on last column

slate-edit-table's People

Contributors

aunswjx avatar banb4n avatar gaeldestrem avatar ingro avatar mklabs avatar samypesse avatar soreine avatar yangchristian avatar zhouzi avatar zhujinxuan 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

slate-edit-table's Issues

Update to New Schema Rules

Warning: The 'types' schema validation rule was changed. Please use the new 'match' syntax with 'type'

Schema rules have changed so the Schema.js file in this repo will need updating.

Cannot copy/paste between two cells

Copy pasting several blocks leads to an error (you can create two paragraphs inside a cell by pressing Shift+Enter)

Uncaught Error: Could not find a node with key "73".
    at Document.assertNode (bundle.js:42011)
    at Document.getPath (bundle.js:43299)
    at Document.object.(/slate-edit-table/anonymous function) [as getPath] (https://gitbookio.github.io/slate-edit-table/bundle.js:39426:28)
    at Changes$2.moveNodeByKey (bundle.js:47178)
    at Change.call (bundle.js:51950)
    at Change.(/slate-edit-table/anonymous function) [as moveNodeByKey] (https://gitbookio.github.io/slate-edit-table/bundle.js:52054:15)
    at bundle.js:46223
    at List.__iterate (bundle.js:5942)
    at List.forEach (bundle.js:8117)

noBlocksWithinCell normalization fails for multiple nested blocks

Repro:

  • Add a peer nested block to tests/schema-no-blocks-within-cells/input.yaml:
document:
  nodes:
    - object: block
      type: table
      data:
          presetAlign:
              - left
              - left
              - left
      nodes:
        # Row 1
        - object: block
          type: table_row
          nodes:
            - object: block
              type: table_cell
              nodes:
                - object: block
                  type: paragraph
                  nodes:
                    - object: text
                      leaves:
                        - text: "Row 1, Col 1"
#########################
# Extra peer nested block
#########################
                - object: block
                  type: paragraph
                  nodes:
                    - object: text
                      leaves:
                        - text: "Extra text"
            - object: block
              type: table_cell
              nodes:
                - object: inline
                  type: link
                  nodes:
                    - object: text
                      leaves:
                        - text: "Row 1, Col 2"
            - object: block
              type: table_cell
              nodes:
                - object: text
                  leaves:
                    - text: "Row 1, Col 3"

Result:

TypeError: Cannot read property 'getChild' of null
      at Changes.unwrapNodeByKey (node_modules\slate\lib\changes\by-key.js:700:20)
      at Change.call (node_modules\slate\lib\models\change.js:197:10)
      at Change.(anonymous function) [as unwrapNodeByKey] (node_modules\slate\lib\models\change.js:262:15)
      at C:/htdocs/slate-edit-table/lib/validation/validateNode.js:82:28
      at List.__iterate (node_modules\immutable\dist\immutable.js:2206:13)
      at List.forEach (node_modules\immutable\dist\immutable.js:4381:19)
      at C:/htdocs/slate-edit-table/lib/validation/validateNode.js:81:29
      at List.__iterate (node_modules\immutable\dist\immutable.js:2206:13)
      at List.forEach (node_modules\immutable\dist\immutable.js:4381:19)
      at Object.normalize (C:/htdocs/slate-edit-table/lib/validation/validateNode.js:80:26)
      at C:/htdocs/slate-edit-table/lib/validation/validateNode.js:55:31
      at iterate (node_modules\slate\lib\changes\with-schema.js:164:5)
      at normalizeNode (node_modules\slate\lib\changes\with-schema.js:185:3)
      at normalizeNodeAndChildren (node_modules\slate\lib\changes\with-schema.js:127:5)
      at _loop (node_modules\slate\lib\changes\with-schema.js:100:7)
      at normalizeNodeAndChildren (node_modules\slate\lib\changes\with-schema.js:122:5)
      at _loop (node_modules\slate\lib\changes\with-schema.js:100:7)
      at normalizeNodeAndChildren (node_modules\slate\lib\changes\with-schema.js:122:5)
      at _loop (node_modules\slate\lib\changes\with-schema.js:100:7)
      at normalizeNodeAndChildren (node_modules\slate\lib\changes\with-schema.js:122:5)
      at Changes.normalizeNodeByKey (node_modules\slate\lib\changes\with-schema.js:54:3)
      at Change.call (node_modules\slate\lib\models\change.js:197:10)
      at Change.(anonymous function) [as normalizeNodeByKey] (node_modules\slate\lib\models\change.js:262:15)
      at Changes.normalizeDocument (node_modules\slate\lib\changes\with-schema.js:37:10)
      at Change.call (node_modules\slate\lib\models\change.js:197:10)
      at Change.(anonymous function) [as normalizeDocument] (node_modules\slate\lib\models\change.js:262:15)
      at Changes.normalize (node_modules\slate\lib\changes\with-schema.js:24:10)
      at Change.call (node_modules\slate\lib\models\change.js:197:10)
      at Change.(anonymous function) [as normalize] (node_modules\slate\lib\models\change.js:262:15)
      at exports.default (C:/htdocs/slate-edit-table/tests/schema-no-blocks-within-cells/change.js:2:19)
      at Context.<anonymous> (C:/htdocs/slate-edit-table/tests/all.js:39:31)

I think we just need to pass the { normalize: false } option to change.unwrapNodeByKey() at https://github.com/GitbookIO/slate-edit-table/blob/master/lib/validation/validateNode.js#L82 just like all the other normalization changes to get around ianstormtaylor/slate#1203. In fact, making that change locally seems to fix the error for me.

I'd be happy to submit a PR with working tests if you like. Thanks!

Option to override default cell node instead of text

  • So that we can add image, multiline text into table cell
  • My suggestion: Add defaultCellNode to insertTable opts & pass down it into createCell function
insertTable(opts, transform, columns = 2, rows = 2)

If opts contains the defaultCellNode node, it will render table-cell's nodes as

nodes: [node]

else it will render

nodes: [
    Slate.Raw.deserializeText({
        kind: 'text',
        text: text
    }, { terse: true })
]

Function Name for `Selection Absolutely Not in Table`;

Hi, some operations (for example change.insertTable) may have unwise behavior when startBlock.type === "cell" && endBlock.tyope !== "cell" . Therefore, I am thinking about implement a function like IsSelectionBothEndNotInTable, but the function name is ugly. Do you have any suggestion about the function?

Thank you a lot,
Jinxuan Zhu

Update: found name IsSelectionOutOfTable... Sorry for messing up your timeline. I will make a PR about the function.

Inserting new table with cols/rows not working

return EditTable()
.transforms
.insertTable(editorState.transform(), parseInt(data.cols, 10), parseInt(data.rows, 10))
.apply();

This code simply creates a table with 1 row and 1 cell, even though data.rows and data.cols is populated and greater than 1.

How to pass props / data to node table_row?

e.g. I want to make a header row

const TableSchema = {
    nodes: {
       ...
        table_row:  props => {
          return props.node.get('data').get('isHeader') ?
            <th {...props.attributes}>{props.children}</th> :
            <tr {...props.attributes}>{props.children}</tr>
        },
     ...
};

I should be able to do something like

// set row as header
plugin.transforms.setRowData(state.transform(), { isHeader: true })
      .apply()

I also may want to have other types of data e.g. isSuccessRow, isSortable, cellColor, onClickCell or some other formatting of the row / cell.

To have this kind of flexibility, I believe that a method would have to be exposed which would allow for any kind of props to be passed to the row / cell / table node

    transform.setNodeByKey(table.key, {
        data: data
    });

Would you like width for table_cell?

Hi, recently I am trying to add presetWidth for table.data and percentWidth for cell.data, and I am deciding whether I shall put it in another plugin (a plugin of slate-edit-table plugin lol).

Just wondering would you like to merge the feature into this plugin when completed? If you like, I will work directly on this plugin and submit as a PR.

Columns normalization might create nodes with conflicting keys

When normalizing tables, we create Nodes with deserializeNode (1) (2) and insert them in the table with setNodeByKey. But setNodeByKey does not re-key the created nodes to avoid key collisions.

A suggestion is to add them using insertFragmentAtRange. But it "has a few weird behaviors that make sense for fragments (like copy/paste) but not as much for programmatic inserting". So it might need some work around. The first node and last nodes are merged with their surrounding nodes, like how during copy/paste it adds the first paragraph’s text into the target node. So we might need to add an extra fake node at the start/end, in order to user insertFragment.

(1):

return Slate.Raw.deserializeNode({

(2):
return Slate.Raw.deserializeNode({

Support multiple cell types

I have plugin set up like this:

const tablePlugin = PluginEditTable({
  typeTable: ['table'],
  typeRow: ['table_row'],
  //<th> and <td> elements
  typeCell: ['table_cell', 'table_heading']
});

adding this parameter object for PluginEditTable like above breaks the functionality of the isSelectionInTable function, with this breaking most of the plugin functionality as it can't recognize the table. However removing that object parameter from PluginEditTable fixes it:

const tablePlugin = PluginEditTable();

This problem is encountered when I want to deserialize table with multiple cell types like and ...
so I need to define multiple typeCell types.
Anyone has ideas how to fix this?

consider adding keyword support to inserting

Right now you can either specify the index exactly, or omit it to mean "after the current one". I think it would be nice if there was an option for keywords:

'before' // before the current column/row
'after' // after the current column/row
'start' // at the start of the columns/rows
'end' // at the end of the columns/rows
insertColumn(transform, 'before')
insertRow(transform, 'end')

The 'start' keyword is equivalent to 0, but all of the others are harder to compute in the moment, and could save lots of extra work.

Issue when deleting a table and the document contains nothing else

Do you want to request a feature or report a bug?

Bug

What's the current behaviour?

  1. Go to https://gitbookio.github.io/slate-edit-table/
  2. Delete everything
  3. Insert a table
  4. Delete that table

From this point on, any subsequent editing fails.

The reason seems to be that the document is left without any nodes.

What's the expected behavior?

The editor should contain the default empty node.

My workaround right now is to manually insert an empty paragraph before deleting the table.

Adjacent orphan cells/rows should be normalized inside a single row/table

The PR #17 added a schema rule that prevent orphan cells or rows to exist, and wrap them in tables.
An enhancement would be to wrap adjacent cells or rows together.

The current behavior:

- cell
- cell

gives

- table
  - row
    - cell
- table
  - row
    - cell

Improved behavior

- cell
- cell

should give

- table
  - row
    - cell
    - cell

Don't use "setNodeByKey" and test with upcoming Slate

We should no longer use setNodeByKey for updating a node, and instead use transforms like insertNodeByKey and removeNodeByKey.

Also since we are going to integrate it in the editor with Slate 0.15 (upcoming), we should test it and adapt it.

Adding table in creates a new div, not table, but that div serializes to a table?

I copied the implementation from the example file. I'm really confused by how this outcome is possible. I'm using Slate 0.16.18 and slate-edit-table 0.8.4 in Electron 1.4.15

Slate generated HTML:

<div data-slate-editor="true" contenteditable="true" autocorrect="true" spellcheck="true" style="outline: none; white-space: pre-wrap; word-wrap: break-word; -webkit-user-modify: read-write-plaintext-only;" role="textbox">
   <p data-key="1"><span data-key="0"><span data-offset-key="0-0">Paragraph</span></span></p>
   <div data-key="243" style="position: relative;">
      <div data-key="237" style="position: relative;">
         <div data-key="234" style="position: relative;"><span data-key="233"><span data-offset-key="233-0"><br></span></span></div>
         <div data-key="236" style="position: relative;"><span data-key="235"><span data-offset-key="235-0"><br></span></span></div>
      </div>
      <div data-key="242" style="position: relative;">
         <div data-key="239" style="position: relative;"><span data-key="238"><span data-offset-key="238-0">Some Text</span></span></div>
         <div data-key="241" style="position: relative;"><span data-key="240"><span data-offset-key="240-0"><br></span></span></div>
      </div>
   </div>
   <p data-key="257"><span data-key="256"><span data-offset-key="256-0"><br></span></span></p>
</div>

HTML Serializer Output:

<p>Paragraph</p>
<table>
   <tbody>
      <tr>
         <td></td>
         <td></td>
      </tr>
      <tr>
         <td>Some Text</td>
         <td></td>
      </tr>
   </tbody>
</table>

Any ideas how this outcome is possible?

hitting tab in last cell should select first cell in new row?

Right now if you hit tab in the last cell of a table, it will create a new row below the current one, and move the selection to the new last cell of the table. Instead, I think it should move it to the first cell in that new row, mimicking how tab works normally.

Allow for non typeCell blocks in a cell

The consumers of an editor I am producing requires table cells to

  1. be multiline
  2. allow images
  3. allow block quotes
  4. allow code blocks

The rules in makeSchema block these. From what I can tell, it's to maintain some sort of data integrity with what goes into the cell (which is a good thing) but it's not flexible enough to meet some other more... unusual :) use cases.

It would be nice if we could specify acceptable types of blocks that may go into a table cell as part of the plugin configuration.

Alignment in the example not working

Hi, I am using this plugin based on [email protected]. I have some problems about the changes.setColumnAlign.

To represent the data.align in the table, I modified the renderNode by

    [tableCell]: props => {
      let align = props.node.get("data").get("align");
      if (!align) {
        const alignsForAllColumns = props.editor.value.document
          .getParent(props.parent.key)
          .get("data")
          .get("align");
        const columnIndex = props.parent.nodes.indexOf(props.node);
        align = alignsForAllColumns[columnIndex];
      }

      align = align || "left";

      return (
        <td style={{ textAlign: align }} {...props.attributes}>
          {props.children}
        </td>
      );
    }

The render function successfully get the data.align from table, but, each time I am using changes.setColumnAlign, the table cells in the same column do not get automatically rendered: like the right column in figure below, the last two rows do not get aligned after setColumnAlign until the cursor moves on to the cells.

screenshot 2017-11-14 12 51 27

Is there any functions to forcibly re-render the table?

Thank you a lot for this elegant and well-developed package,
Jinxuan Zhu

Normalization: enforce rows / cells only inside table?

(Excellent plugin; this one was really the tipping point in my switch to Slate after struggling to handle table abstractions myself within the quirks and API limitations of Draft)

I see that the schema enforces certain normalization rules (in the form of "parent may only contain children of type x"; ie, tables may contain rows, rows may contain cells) but I'm occasionally seeing a situation where a paste operation by the user ends up inserting rows without an enclosing table. I haven't fully explored all the schema normalization / transformation operations available in slate's current iteration, but would it be possible to include rules of the type "child can only appear within parent type Y" so that these table-fragment paste operations would automatically wrap an enclosing table tag when needed?

Using Local Repo -- Uncaught Error: You must pass a block `type`.

I checked out slate-edit-table and yarn linked into my other app. When I call insertTable I get the error below. I tried this against master and 0.8.4.

block.js:143 Uncaught Error: You must pass a block `type`.
    at Function.create (http://localhost:3808/webpack/frontend.js:31596:35)
    at Object.block (http://localhost:3808/webpack/frontend.js:28144:30)
    at Object.Transforms.insertBlock (http://localhost:3808/webpack/frontend.js:223492:31)
    at Transform.Object.keys.forEach.Transform.(anonymous function) [as insertBlock] (http://localhost:3808/webpack/frontend.js:221152:32)
    at insertTable (http://localhost:3808/webpack/frontend.js:146533:20)
    at Toolbar.insertTable (http://localhost:3808/webpack/frontend.js:150653:19)
    at Toolbar.proxiedMethod (http://localhost:3808/webpack/frontend.js:214867:30)
    at GridSelect.insertTable (http://localhost:3808/webpack/frontend.js:150048:18)
    at GridSelect.proxiedMethod (http://localhost:3808/webpack/frontend.js:214867:30)
    at Object.ReactErrorUtils.invokeGuardedCallback (http://localhost:3808/webpack/frontend.js:59996:16)
    at executeDispatch (http://localhost:3808/webpack/frontend.js:38408:21)
    at Object.executeDispatchesInOrder (http://localhost:3808/webpack/frontend.js:38428:7)
    at executeDispatchesAndRelease (http://localhost:3808/webpack/frontend.js:24554:22)
    at executeDispatchesAndReleaseTopLevel (http://localhost:3808/webpack/frontend.js:24565:10)
    at Array.forEach (native)
    at forEachAccumulated (http://localhost:3808/webpack/frontend.js:97619:9)
    at Object.processEventQueue (http://localhost:3808/webpack/frontend.js:24768:7)
    at runEventQueueInBatch (http://localhost:3808/webpack/frontend.js:204913:18)
    at Object.handleTopLevel [as _handleTopLevel] (http://localhost:3808/webpack/frontend.js:204924:5)
    at handleTopLevelImpl (http://localhost:3808/webpack/frontend.js:205006:24)
    at ReactDefaultBatchingStrategyTransaction.perform (http://localhost:3808/webpack/frontend.js:38771:20)
    at Object.batchedUpdates (http://localhost:3808/webpack/frontend.js:96405:26)
    at Object.batchedUpdates (http://localhost:3808/webpack/frontend.js:17126:27)
    at dispatchEvent (http://localhost:3808/webpack/frontend.js:205081:20)

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.