GetmeUK / ContentTools

A JS library for building WYSIWYG editors for HTML content.
http://getcontenttools.com
MIT License
3.94k stars 393 forks source link

Trouble editing link text #558

Open ScotsScripts opened 4 years ago

ScotsScripts commented 4 years ago

I have this:

<div data-editable data-name="sbt-editlink">
 <a href="/">Edit This Link Text</a>
</div>

When I click the "pencil" the editor adds a P tag and the "edit this link text" is not editable.

<div data-editable data-name="sbt-editlink">
<a href="/" class="ce-element ce-element--type-static">Edit This Link Text</a>
<p class="ce-element--empty ce-element ce-element--type-text"></p>
</div>

Before I go any further, is what I'm trying to do possible with contenttools? I'd like to hard code links but allow editing of the link text.

anthonyjb commented 4 years ago

@ScotsScripts sort of - the <a> is an inline element that CT wont recognise, so you can:

ScotsScripts commented 4 years ago

I can't find any documentation on fixtures. When I added "data-fixture" to the A tag the editor wouldn't start. Do you have any examples of click-to-edit link text anywhere?

ScotsScripts commented 4 years ago

Is there any documentation at all on using fixtures without trying to decypher the sandbox code? Something simple with, "This is how it's done" style information?

anthonyjb commented 4 years ago

There's some basic info on the initialising the editor for fixtures on this page: http://getcontenttools.com/api/content-tools (search for fixture).

The sandbox demo within the project repo includes a fixture example in the project.

That's all there is right now I'm afraid.

ScotsScripts commented 4 years ago

Problem is the sandbox.js is quite different then your tutorial js and I'm feeling frustrated trying to figure out how to get the fixtures from the sandbox.js to work in the tutorial style js. Any hints?

anthonyjb commented 4 years ago

@ScotsScripts can you post the code you have for initializing your editor please?

ScotsScripts commented 4 years ago

I feel like I'm getting closer, however this line:

editor.init( '*[data-editable]', '[data-fixture]', 'data-name');

gets this error:

uncaught TypeError: this._fixtureTest is not a function

Here's what I have. The editing, saving, and image uploading/manipulation all work (not now, but before I started added fixture stuff.) Unfortunately I'm not even close to an expert js developer. Instead I rely on examples and tutorials and for Contenttools there just isn't that much stuff out there.

Thanks for taking a look at this.

window.addEventListener('load', function() {
    var FIXTURE_TOOLS, IMAGE_FIXTURE_TOOLS, LINK_FIXTURE_TOOLS, editor;
    editor = ContentTools.EditorApp.get();
    editor.init('*[data-editable]', '[data-fixture]', 'data-name');
    ContentEdit.ENABLE_DRAG_CLONING = true;
    ContentTools.IMAGE_UPLOADER = imageUploader;
    ContentTools.StylePalette.add([
        new ContentTools.Style('Image Fluid', 'img-fluid', ['img'])
    ]);
    FIXTURE_TOOLS = [
        ['undo', 'redo', 'remove']
    ];
    IMAGE_FIXTURE_TOOLS = [
        ['undo', 'redo', 'image']
    ];
    LINK_FIXTURE_TOOLS = [
        ['undo', 'redo', 'link']
    ];
    return ContentEdit.Root.get().bind('focus', function(element) {
        var tools;
        if (element.isFixed()) {
            if (element.type() === 'ImageFixture') {
                tools = IMAGE_FIXTURE_TOOLS;
            } else if (element.tagName() === 'a') {
                tools = LINK_FIXTURE_TOOLS;
            } else {
                tools = FIXTURE_TOOLS;
            }
        } else {
            tools = ContentTools.DEFAULT_TOOLS;
        }
        if (editor.toolbox().tools() !== tools) {
            return editor.toolbox().tools(tools);
        }
    });
    editor.addEventListener('start', function(ev) {
        var _this = this;
        // Call save every 30 seconds
        function autoSave() {
            _this.save(true);
        };
        this.autoSaveTimer = setInterval(autoSave, 30 * 1000);
    });
    editor.addEventListener('stop', function(ev) {
        // Stop the autosave
        clearInterval(this.autoSaveTimer);
    });
    editor.addEventListener('saved', function(ev) {
        var name, payload, regions, xhr;
        // Check that something changed
        regions = ev.detail().regions;
        if (Object.keys(regions).length == 0) {
            return;
        }
        // Set the editor as busy while we save our changes
        this.busy(true);
        // Collect the contents of each region into a FormData instance
        payload = new FormData();
        for (name in regions) {
            if (regions.hasOwnProperty(name)) {
                payload.append(name, regions[name]);
            }
        }
        // Send the update content to the server to be saved
        function onStateChange(ev) {
            // Check if the request is finished
            if (ev.target.readyState == 4) {
                editor.busy(false);
                if (ev.target.status == '200') {
                    // Save was successful, notify the user with a flash
                    new ContentTools.FlashUI('ok');
                } else {
                    // Save failed, notify the user with a flash
                    new ContentTools.FlashUI('no');
                }
            }
        };
        xhr = new XMLHttpRequest();
        xhr.addEventListener('readystatechange', onStateChange);
        xhr.open('POST', '/save_article');
        xhr.send(payload);
    });

    function imageUploader(dialog) {
        var image, xhr, xhrComplete, xhrProgress;
        dialog.addEventListener('imageuploader.fileready', function(ev) {
            // Upload a file to the server
            var formData;
            var file = ev.detail().file;
            // Define functions to handle upload progress and completion
            xhrProgress = function(ev) {
                // Set the progress for the upload
                dialog.progress((ev.loaded / ev.total) * 100);
            }
            xhrComplete = function(ev) {
                var response;
                // Check the request is complete
                if (ev.target.readyState != 4) {
                    return;
                }
                // Clear the request
                xhr = null
                xhrProgress = null
                xhrComplete = null
                // Handle the result of the upload
                if (parseInt(ev.target.status) == 200) {
                    // Unpack the response (from JSON)
                    response = JSON.parse(ev.target.responseText);
                    // Store the image details
                    image = {
                        size: response.size,
                        url: response.url
                    };
                    // Populate the dialog
                    dialog.populate(image.url, image.size);
                } else {
                    // The request failed, notify the user
                    new ContentTools.FlashUI('no');
                }
            }
            // Set the dialog state to uploading and reset the progress bar to 0
            dialog.state('uploading');
            dialog.progress(0);
            // Build the form data to post to the server
            formData = new FormData();
            formData.append('image', file);
            // Make the request
            xhr = new XMLHttpRequest();
            xhr.upload.addEventListener('progress', xhrProgress);
            xhr.addEventListener('readystatechange', xhrComplete);
            xhr.open('POST', '/image_upload', true);
            xhr.send(formData);
        });

        function rotateImage(direction) {
            // Request a rotated version of the image from the server
            var formData;
            // Define a function to handle the request completion
            xhrComplete = function(ev) {
                var response;
                // Check the request is complete
                if (ev.target.readyState != 4) {
                    return;
                }
                // Clear the request
                xhr = null
                xhrComplete = null
                // Free the dialog from its busy state
                dialog.busy(false);
                // Handle the result of the rotation
                if (parseInt(ev.target.status) == 200) {
                    // Unpack the response (from JSON)
                    response = JSON.parse(ev.target.responseText);
                    // Store the image details (use fake param to force refresh)
                    image = {
                        size: response.size,
                        url: response.url + '?_ignore=' + Date.now()
                    };
                    console.log('size: ' + response.size);
                    console.log('url: ' + response.url);
                    // Populate the dialog
                    dialog.populate(image.url, image.size);
                } else {
                    // The request failed, notify the user
                    new ContentTools.FlashUI('no');
                }
            }
            // Set the dialog to busy while the rotate is performed
            dialog.busy(true);
            // Build the form data to post to the server
            formData = new FormData();
            formData.append('url', image.url);
            formData.append('direction', direction);
            // Make the request
            xhr = new XMLHttpRequest();
            xhr.addEventListener('readystatechange', xhrComplete);
            xhr.open('POST', '/image_rotate', true);
            xhr.send(formData);
        }
        dialog.addEventListener('imageuploader.rotateccw', function() {
            rotateImage('CCW');
        });
        dialog.addEventListener('imageuploader.rotatecw', function() {
            rotateImage('CW');
        });
        dialog.addEventListener('imageuploader.save', function() {
            var crop, cropRegion, formData;
            // Define a function to handle the request completion
            xhrComplete = function(ev) {
                // Check the request is complete
                if (ev.target.readyState !== 4) {
                    return;
                }
                // Clear the request
                xhr = null
                xhrComplete = null
                // Free the dialog from its busy state
                dialog.busy(false);
                // Handle the result of the rotation
                if (parseInt(ev.target.status) === 200) {
                    // Unpack the response (from JSON)
                    var response = JSON.parse(ev.target.responseText);
                    // Trigger the save event against the dialog with details of the
                    // image to be inserted.
                    dialog.save(
                        response.url,
                        response.size, {
                            'alt': response.alt,
                            'data-ce-max-width': response.size[0]
                        });
                } else {
                    // The request failed, notify the user
                    new ContentTools.FlashUI('no');
                }
            }
            // Set the dialog to busy while the rotate is performed
            dialog.busy(true);
            // Build the form data to post to the server
            formData = new FormData();
            formData.append('url', image.url);
            // Set the width of the image when it's inserted, this is a default
            // the user will be able to resize the image afterwards.
            formData.append('width', 600);
            // Check if a crop region has been defined by the user
            if (dialog.cropRegion()) {
                formData.append('crop', dialog.cropRegion());
            }
            // Make the request
            xhr = new XMLHttpRequest();
            xhr.addEventListener('readystatechange', xhrComplete);
            xhr.open('POST', '/image_rotate2');
            xhr.send(formData);
        });
        dialog.addEventListener('imageuploader.clear', function() {
            // Clear the current image
            dialog.clear();
            image = null;
        });
        dialog.addEventListener('imageuploader.cancelupload', function() {
            // Cancel the current upload
            // Stop the upload
            if (xhr) {
                xhr.upload.removeEventListener('progress', xhrProgress);
                xhr.removeEventListener('readystatechange', xhrComplete);
                xhr.abort();
            }
            // Set the dialog to empty
            dialog.state('empty');
        });
    }
});
anthonyjb commented 4 years ago

Can you try changing the line to:

editor.init('[data-editable], [data-fixture]', 'data-name');

The first argument is a CSS query that should capture both editable and fixture regions.

ScotsScripts commented 4 years ago

That definitely helped quite a bit, the link fixtures seem to be working. That solves the issue of trying to make it easy to edit link button text/targets. What a relief!

I'm also trying to get image fixtures to work. The code below is basically the same as the sandbox but when I click the pencil icon to edit the image in this code disappears from view as well as from the source. The background image style code is still in there as a background url but not actually loading (it does exist on the path.)

                <div
                   data-ce-tag="img-fixture"
                   data-fixture
                   style="background-image: url('/images_elements/image1.png');"
                   class="image-fixture"
                   >
                   <img src="/images_elements/image1.png" alt="Some image">
                </div>
ScotsScripts commented 4 years ago

Fixtures are a pain. It took me quite some time to finally figure out that a fixture returns the entire element rather than just the part that's being edited.

How do you deal with this? Do you have code in your back end that parses fixtures differently than non-fixtures?

The way I'm doing it for "normal" editable areas is saving each chunk in a db and then replacing stuff in the master template. However with fixtures it appears I'll need to figure out a way to replace the entire <a element because that's what is getting passed to my ajax processing file to be saved.

anthonyjb commented 4 years ago

I save fixtures in the same way I save regions (e.g whatever the output of the html method against the root node for the region or fixture is). In my template my code looks as follows (jinja template for reference):

Region

<div
    data-cf-snippet="{{ snippet.id }}"
    data-editable
    data-name="snippet:{{ snippet.id }}:content"
    >
    {% if snippet.contents.content %}
        {{ snippet.contents.content|safe }}
    {% else %}
        <p>Enter content...</p>
    {% endif %}
</div>

Fixture

{% if snippet.contents.desc %}
    {{ snippet.contents.desc|safe }}
{% else %}
    <p
        data-fixture
        data-ce-tag="p"
        data-name="snippet:{{ snippet.id }}:desc"
        class="intro__desc"
        >
        Enter content...
    </p>
{% endif %}

As you can see the only real difference is that with fixture the template inserts the content in the root region element where as with fixtures the whole root element is replaced when. In both cases if there's not content in the database for the region or fixture a default placeholder to get the user started is provided.

ScotsScripts commented 4 years ago

Thanks, that gives me an idea and I won't have to break what I've already done too much.

ScotsScripts commented 4 years ago

Do you ever save data simply as a flat file in the page or do you always save it in chunks in a db?

anthonyjb commented 4 years ago

I've done both before, the content tools website is an example of where I save the content in the editor directly into the template, through I more commonly use the db as that's the approach out in-house CMS takes.

ScotsScripts commented 4 years ago

Contenttools is set up to send the "regions" to the ajax page, does it also send the entire page? I'm thinking about saving as a flat file but was hoping to not have to parse it out for replacing region data one at a time.

anthonyjb commented 4 years ago

This tutorial actual covers the approach I take to saving content on the ContentTools - http://getcontenttools.com/tutorials/saving-strategies

I don't save the entire page, just the regions, but I update those regions within the HTML template itself.

ScotsScripts commented 4 years ago

Thanks again for your help!