npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

wyg

v0.0.30

Published

A new WYSIWYG editing experience for the modern web.

Downloads

17

Readme

Wyg Editor

ATTENTION: WORK IN PROGRESS. NOT MEANT TO BE USED BY ANYONE — STILL IN DEVELOPMENT PHASE, MANY BUGS TO FIX

A new WYSIWYG editing experience for the modern web.

  • Built entirely from scratch
  • Works better than anything before
  • Minimalistic UI, blazingly fast (works with DOM nodes directly)
  • Normalizes entered markup on the fly (splits paragraphs with linebreaks)
  • Smart linebreak management (no Shift-Enter required to put a newline)
  • Drag & drop media between paragraphs
  • Insert media by simply pasting links into text
  • Arranges media nicely in columns
  • Fluid layout with animations
  • Floating markup panel with custom tags support
  • Serializes to/from JSON
  • Custom undo/redo manager (works with arbitrary DOM changes)
  • Intercepts native undo/redo commands (not limited to hotkeys)
  • Pluggable/extendable architecture based on traits
  • Test-driven

👉 Watch demo on YouTube

pic

Recent news

TODO

In it's current state the stuff is quite unusable. Things to be done:

  • [ ] Safari compatibility (some tests fail).
  • [ ] MS Edge compatibility.
  • [ ] Implement hyperlink editing in the floating markup panel.
  • [x] Fix nonworking plus button (should trigger file open dialog).
  • [ ] Add build script.

Minor (but important) tasks:

  • [x] Implement ability to insert text paragraphs at arbitrary location around media rows (see demo video).
  • [ ] Get rid of jQuery (moving to the Node+ library)
  • [ ] Make use of $depends mechanism for traits.

Running demo

You will need node and npm.

  1. Clone repo with git clone http://github.com/xpl/wyg
  2. Run npm install to install dependencies.
  3. Run node demo.js
  4. Open localhost:1333 in Chrome (Safari / Firefox / Edge support is coming)

Updating demo

Instead of git pull, use ./update.sh (runs git pull && npm update). This is needed because it's dependencies change rapidly and you need to update all shit together to maintain consistency.

Under the hood

Everything is built upon a JS library called Useless.js (working title). It delivers composable traits support to JavaScript and a powerful unit test system. You may read more about it in the project's wiki. DOM operations are based on the Node+ library (coming with Useless).

Setting value

Editor's state is exposed via the value property:

wyg.value = [
    { type: 'p', html: 'this is <b>text paragraph</b>, containing arbitrary HTML' },
    { type: 'media', // media row
      media: [
          { type: 'img', // media element
            src:  'http://example/some-image.jpg',
            originalSize: { width: 1280, height: 720 } },
          
          { type: 'iframe', // media element
            src:  '...'
            originalSize: ... }
      ]
    },
    ...
  ]

Interpreting value

When reading value, some additional metadata is returned on media elements:

  { type: 'img',
    src:  '...'
    originalSize: ...
    relativeSize: {
        width:  0.5,   // relative to page width
        height: 0.247  // relative to element width
    }
  }

Relative size encodes the calculated size of a media item, relative to page width. It is abstract from absolute metrics and screen sizes, so a responsive layout could be generated from that schema.

Here's how you can describe an element which height is encoded as a percentage of its width, with pure CSS:

<media-row>
  <media-item style="width: 50%;">
      <spacer style="padding-top: 24.7%;"></spacer>
      <content style="background-image:url(some-image.jpg);"></content>
  </media-item>
  ...
</media-row>
media-row          { display: block; white-space: nowrap; overflow: hidden; }
media-item         { display: inline-block; position: relative; overflow: hidden; }
media-item spacer  { display: block; background-size: cover; }
media-item content { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

It works because padding percentages are relative to element's width, even with padding-top. So a height can be encoded via an additional spacer element put inside.

Implementing the file uploading

See the reference implementation at the file_uploading.js trait. You will need to implement the uploadFile method.

With a Promise, it should return elements instantiated by the renderMedia factory:

uploadFile: function (file, then) {
                return JSONAPI.uploadFile ('/uploads', file).then (this.$ (function (response) {
                    return this.renderMedia ({ type: 'img',
                                                src: '/uploads/' + response.file + '.jpg',
                                       originalSize: { width: response.w,
                                                      height: response.h } }) })).panic },

See instructions below on how to extend the renderMedia method behavior.

Adding support of new media types

All incoming URLs that are pasted from clipboard go through parseMedia facility. This function converts URLs to abstract media definitions in JSON format. Those definitions, when serialized, can be easily stored/interpreted by external applications (e.g. template engines, when rendering to static HTML at server side).

Example:

this.parseMedia ('https://www.youtube.com/watch?v=JQ0qgyCuoCw')
    .then (x => console.log (x))

Rendered output will be:

{ type: 'iframe',
  src:   '...',
  originalUrl: 'https://www.youtube.com/watch?v=JQ0qgyCuoCw',
  originalSize: { width: ..., height: ... } }

This is then feeded to the media rendering facility, which processes those definitions, producing DOM elements:

this.renderMedia (def) // produces DOM element from that definition

Both functions can be extended to introduce new behavior. This is how you do that.

1. Extending parseMedia

Tag a method with $parseMedia to designate it as an URL parser. For asynchronous parsing, you can return Promise:

images: $parseMedia (function (url) {
            return Image.fetch (url)
                        .then (function (img) {
                                return { type: 'img',
                                          src:  url,
                                 originalSize: { width: img.width,
                                                height: img.height } } }) }),
youtube: $parseMedia (function (url) {
            var match = url.match(/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/)
            var id = match && (match[7].length == 11) && match[7]
            if (id) {
                return {
                    type: 'iframe',
                     src: '//www.youtube.com/embed/' + id + '?wmode=transparent',
            originalSize: { width: 16000, // numbers here encode aspect ratio + max size (not actual onscreen size)
                           height: 9000 } } } }),

You can also use tag groups (a feature of every $prototype) to reduce clutter in case of many parser methods:

$parseMedia: {
    images: ...
    youtube: ...
    vimeo: ...
    soundcloud: ... }

You can query what types are supported by checking static supportedMedia property. It will be generated from method names:

MyEditor.supportedMedia // ['images', 'youtube', ...]

For those who are curious, here's the actual implementation of the parseMedia factory:

parseMedia: function (url) {

    /*  Gather values from all methods tagged with $parseMedia   */

        var values = _.map (this.constructor.$membersByTag.parseMedia,
                                function (def, name) {
                                    return this[name] (url) || Promise.reject (null) }, this)
    
        return Promise.firstResolved (values)
                      .then (function (media) {
                                return _.extend (media, { originalUrl: url }) }) },

2. Extending renderMedia

This is done much the same way as with the former one:

img: $renderMedia (function (media) {
        return N.img.attr ({ src: media.src,
                           width: media.originalSize.width,
                          height: media.originalSize.height }) }),
iframe: $renderMedia (function (media) {
            return N.div.add (
                   N.iframe.attr ({ src: media.src, frameborder: 0, allowfullscreen: true })) } }),

Those methods are dispatched by looking into the type property in media definitions.

Changing default icons

Default icons are hard-coded as SVG HTML, and you can change them them by overriding these methods:

makeWaitIcon: function () {
                return N.div.extend ({ className: 'wyg-icon', innerHTML: '<svg>...' }) },

makeAddIcon: function () {
                return N.div.extend ({ className: 'wyg-icon', innerHTML: '<svg>...' }) },

Expected return value is a DOM node. For example, returning an FontAwesome icon (some CSS tweaks may be required):

make: function () {
        return N.div.cls ('wyg-icon fa fa-plus-square') },