krafthaus/bauhaus

View on GitHub
bower_components/tinymce/plugins/textpattern/plugin.js

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * plugin.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/*global tinymce:true */

tinymce.PluginManager.add('textpattern', function(editor) {
    var isPatternsDirty = true, patterns;

    patterns = editor.settings.textpattern_patterns || [
        {start: '*', end: '*', format: 'italic'},
        {start: '**', end: '**', format: 'bold'},
        {start: '#', format: 'h1'},
        {start: '##', format: 'h2'},
        {start: '###', format: 'h3'},
        {start: '####', format: 'h4'},
        {start: '#####', format: 'h5'},
        {start: '######', format: 'h6'},
        {start: '1. ', cmd: 'InsertOrderedList'},
        {start: '* ', cmd: 'InsertUnorderedList'},
        {start: '- ', cmd: 'InsertUnorderedList'}
    ];

    // Returns a sorted patterns list, ordered descending by start length
    function getPatterns() {
        if (isPatternsDirty) {
            patterns.sort(function(a, b) {
                if (a.start.length > b.start.length) {
                    return -1;
                }

                if (a.start.length < b.start.length) {
                    return 1;
                }

                return 0;
            });

            isPatternsDirty = false;
        }

        return patterns;
    }

    // Finds a matching pattern to the specified text
    function findPattern(text) {
        var patterns = getPatterns();

        for (var i = 0; i < patterns.length; i++) {
            if (text.indexOf(patterns[i].start) !== 0) {
                continue;
            }

            if (patterns[i].end && text.lastIndexOf(patterns[i].end) != text.length - patterns[i].end.length) {
                continue;
            }

            return patterns[i];
        }
    }

    // Finds the best matching end pattern
    function findEndPattern(text, offset, delta) {
        var patterns, pattern, i;

        // Find best matching end
        patterns = getPatterns();
        for (i = 0; i < patterns.length; i++) {
            pattern = patterns[i];
            if (pattern.end && text.substr(offset - pattern.end.length - delta, pattern.end.length) == pattern.end) {
                return pattern;
            }
        }
    }

    // Handles inline formats like *abc* and **abc**
    function applyInlineFormat(space) {
        var selection, dom, rng, container, offset, startOffset, text, patternRng, pattern, delta, format;

        function splitContainer() {
            // Split text node and remove start/end from text node
            container = container.splitText(startOffset);
            container.splitText(offset - startOffset - delta);
            container.deleteData(0, pattern.start.length);
            container.deleteData(container.data.length - pattern.end.length, pattern.end.length);
        }

        selection = editor.selection;
        dom = editor.dom;

        if (!selection.isCollapsed()) {
            return;
        }

        rng = selection.getRng(true);
        container = rng.startContainer;
        offset = rng.startOffset;
        text = container.data;
        delta = space ? 1 : 0;

        if (container.nodeType != 3) {
            return;
        }

        // Find best matching end
        pattern = findEndPattern(text, offset, delta);
        if (!pattern) {
            return;
        }

        // Find start of matched pattern
        // TODO: Might need to improve this if there is nested formats
        startOffset = Math.max(0, offset - delta);
        startOffset = text.lastIndexOf(pattern.start, startOffset - pattern.end.length - 1);

        if (startOffset === -1) {
            return;
        }

        // Setup a range for the matching word
        patternRng = dom.createRng();
        patternRng.setStart(container, startOffset);
        patternRng.setEnd(container, offset - delta);
        pattern = findPattern(patternRng.toString());

        if (!pattern || !pattern.end) {
            return;
        }

        // If container match doesn't have anything between start/end then do nothing
        if (container.data.length <= pattern.start.length + pattern.end.length) {
            return;
        }

        format = editor.formatter.get(pattern.format);
        if (format && format[0].inline) {
            splitContainer();
            editor.formatter.apply(pattern.format, {}, container);
            return container;
        }
    }

    // Handles block formats like ##abc or 1. abc
    function applyBlockFormat() {
        var selection, dom, container, firstTextNode, node, format, textBlockElm, pattern, walker, rng, offset;

        selection = editor.selection;
        dom = editor.dom;

        if (!selection.isCollapsed()) {
            return;
        }

        textBlockElm = dom.getParent(selection.getStart(), 'p');
        if (textBlockElm) {
            walker = new tinymce.dom.TreeWalker(textBlockElm, textBlockElm);
            while ((node = walker.next())) {
                if (node.nodeType == 3) {
                    firstTextNode = node;
                    break;
                }
            }

            if (firstTextNode) {
                pattern = findPattern(firstTextNode.data);
                if (!pattern) {
                    return;
                }

                rng = selection.getRng(true);
                container = rng.startContainer;
                offset = rng.startOffset;

                if (firstTextNode == container) {
                    offset = Math.max(0, offset - pattern.start.length);
                }

                if (tinymce.trim(firstTextNode.data).length == pattern.start.length) {
                    return;
                }

                if (pattern.format) {
                    format = editor.formatter.get(pattern.format);
                    if (format && format[0].block) {
                        firstTextNode.deleteData(0, pattern.start.length);
                        editor.formatter.apply(pattern.format, {}, firstTextNode);

                        rng.setStart(container, offset);
                        rng.collapse(true);
                        selection.setRng(rng);
                    }
                }

                if (pattern.cmd) {
                    editor.undoManager.transact(function() {
                        firstTextNode.deleteData(0, pattern.start.length);
                        editor.execCommand(pattern.cmd);
                    });
                }
            }
        }
    }

    function handleEnter() {
        var rng, wrappedTextNode;

        wrappedTextNode = applyInlineFormat();
        if (wrappedTextNode) {
            rng = editor.dom.createRng();
            rng.setStart(wrappedTextNode, wrappedTextNode.data.length);
            rng.setEnd(wrappedTextNode, wrappedTextNode.data.length);
            editor.selection.setRng(rng);
        }

        applyBlockFormat();
    }

    function handleSpace() {
        var wrappedTextNode, lastChar, lastCharNode, rng, dom;

        wrappedTextNode = applyInlineFormat(true);
        if (wrappedTextNode) {
            dom = editor.dom;
            lastChar = wrappedTextNode.data.slice(-1);

            // Move space after the newly formatted node
            if (/[\u00a0 ]/.test(lastChar)) {
                wrappedTextNode.deleteData(wrappedTextNode.data.length - 1, 1);
                lastCharNode = dom.doc.createTextNode(lastChar);

                if (wrappedTextNode.nextSibling) {
                    dom.insertAfter(lastCharNode, wrappedTextNode.nextSibling);
                } else {
                    wrappedTextNode.parentNode.appendChild(lastCharNode);
                }

                rng = dom.createRng();
                rng.setStart(lastCharNode, 1);
                rng.setEnd(lastCharNode, 1);
                editor.selection.setRng(rng);
            }
        }
    }

    editor.on('keydown', function(e) {
        if (e.keyCode == 13 && !tinymce.util.VK.modifierPressed(e)) {
            handleEnter();
        }
    }, true);

    editor.on('keyup', function(e) {
        if (e.keyCode == 32 && !tinymce.util.VK.modifierPressed(e)) {
            handleSpace();
        }
    });

    this.getPatterns = getPatterns;
    this.setPatterns = function(newPatterns) {
        patterns = newPatterns;
        isPatternsDirty = true;
    };
});