com.google.gwt.dom.client.StyleInjector.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dom.client.StyleInjector.java

Source

/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dom.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;

/**
 * Used to add stylesheets to the document. The one-argument versions of
 * {@link #inject}, {@link #injectAtEnd}, and {@link #injectAtStart} use
 * {@link Scheduler#scheduleFinally} to minimize the number of individual style
 * elements created.
 * <p>
 * The api here is a bit redundant, with similarly named methods returning
 * either <code>void</code> or {@link StyleElement} &mdash; e.g.,
 * {@link #inject(String) void inject(String)} v.
 * {@link #injectStylesheet(String) StyleElement injectStylesheet(String)}. The
 * methods that return {@link StyleElement} are not guaranteed to work as
 * expected on Internet Explorer. Because they are still useful to developers on
 * other browsers they are not deprecated, but <strong>IE developers should
 * avoid the methods with {@link StyleElement} return values</strong> (at least
 * up until, and excluding, IE10).
 */
public class StyleInjector {
    private static final JsArrayString toInject = JavaScriptObject.createArray().cast();

    private static final JsArrayString toInjectAtEnd = JavaScriptObject.createArray().cast();

    private static final JsArrayString toInjectAtStart = JavaScriptObject.createArray().cast();

    private static ScheduledCommand flusher = new ScheduledCommand() {
        public void execute() {
            if (needsInjection) {
                flush(null);
            }
        }
    };

    private static boolean needsInjection = false;

    /**
     * Flushes any pending stylesheets to the document.
     * <p>
     * This can be useful if you used CssResource.ensureInjected but now in the
     * same event loop want to measure widths based on the new styles.
     * <p>
     * Note that calling this method excessively will decrease performance.
     */
    public static void flush() {
        inject(true);
    }

    /**
     * Add a stylesheet to the document.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     */
    public static void inject(String css) {
        inject(css, false);
    }

    /**
     * Add a stylesheet to the document.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     * @param immediate
     *            if <code>true</code> the DOM will be updated immediately
     *            instead of just before returning to the event loop. Using this
     *            option excessively will decrease performance, especially if
     *            used with an inject-css-on-init coding pattern
     */
    public static void inject(String css, boolean immediate) {
        toInject.push(css);
        inject(immediate);
    }

    /**
     * Add stylesheet data to the document as though it were declared after all
     * stylesheets previously created by {@link #inject(String)}.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     */
    public static void injectAtEnd(String css) {
        injectAtEnd(css, false);
    }

    /**
     * Add stylesheet data to the document as though it were declared after all
     * stylesheets previously created by {@link #inject(String)}.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     * @param immediate
     *            if <code>true</code> the DOM will be updated immediately
     *            instead of just before returning to the event loop. Using this
     *            option excessively will decrease performance, especially if
     *            used with an inject-css-on-init coding pattern
     */
    public static void injectAtEnd(String css, boolean immediate) {
        toInjectAtEnd.push(css);
        inject(immediate);
    }

    /**
     * Add stylesheet data to the document as though it were declared before all
     * stylesheets previously created by {@link #inject(String)}.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     */
    public static void injectAtStart(String css) {
        injectAtStart(css, false);
    }

    /**
     * Add stylesheet data to the document as though it were declared before all
     * stylesheets previously created by {@link #inject(String)}.
     * 
     * @param css
     *            the CSS contents of the stylesheet
     * @param immediate
     *            if <code>true</code> the DOM will be updated immediately
     *            instead of just before returning to the event loop. Using this
     *            option excessively will decrease performance, especially if
     *            used with an inject-css-on-init coding pattern
     */
    public static void injectAtStart(String css, boolean immediate) {
        toInjectAtStart.unshift(css);
        inject(immediate);
    }

    /**
     * Add a stylesheet to the document.
     * <p>
     * The returned StyleElement cannot be implemented consistently across all
     * browsers. Specifically, <strong>applications that need to run on Internet
     * Explorer should not use this method. Call {@link #inject(String)}
     * instead.</strong>
     * 
     * @param contents
     *            the CSS contents of the stylesheet
     * @return the StyleElement that contains the newly-injected CSS (unreliable
     *         on Internet Explorer)
     */
    public static StyleElement injectStylesheet(String contents) {
        toInject.push(contents);
        return flush(toInject);
    }

    /**
     * Add stylesheet data to the document as though it were declared after all
     * stylesheets previously created by {@link #injectStylesheet(String)}.
     * <p>
     * The returned StyleElement cannot be implemented consistently across all
     * browsers. Specifically, <strong>applications that need to run on Internet
     * Explorer should not use this method. Call {@link #injectAtEnd(String)}
     * instead.</strong>
     * 
     * @param contents
     *            the CSS contents of the stylesheet
     * @return the StyleElement that contains the newly-injected CSS (unreliable
     *         on Internet Explorer)
     */
    public static StyleElement injectStylesheetAtEnd(String contents) {
        toInjectAtEnd.push(contents);
        return flush(toInjectAtEnd);
    }

    /**
     * Add stylesheet data to the document as though it were declared before any
     * stylesheet previously created by {@link #injectStylesheet(String)}.
     * <p>
     * The returned StyleElement cannot be implemented consistently across all
     * browsers. Specifically, <strong>applications that need to run on Internet
     * Explorer should not use this method. Call
     * {@link #injectAtStart(String, boolean)} instead.</strong>
     * 
     * @param contents
     *            the CSS contents of the stylesheet
     * @return the StyleElement that contains the newly-injected CSS (unreliable
     *         on Internet Explorer)
     */
    public static StyleElement injectStylesheetAtStart(String contents) {
        toInjectAtStart.unshift(contents);
        return flush(toInjectAtStart);
    }

    /**
     * Replace the contents of a previously-injected stylesheet. Updating the
     * stylesheet in-place is typically more efficient than removing a
     * previously-created element and adding a new one.
     * <p>
     * This method should be used with some caution as StyleInjector may recycle
     * StyleElements on certain browsers. Specifically, <strong>applications
     * that need to run on Internet Explorer should not use this
     * method. </strong>
     * 
     * @param style
     *            a StyleElement previously-returned from
     *            {@link #injectStylesheet(String)}.
     * @param contents
     *            the new contents of the stylesheet.
     */
    public static void setContents(StyleElement style, String contents) {
        StyleInjectorImpl.IMPL.setContents(style, contents);
    }

    /**
     * The <code>which</code> parameter is used to support the deprecated API.
     */
    private static StyleElement flush(JavaScriptObject which) {
        StyleElement toReturn = null;
        StyleElement maybeReturn;
        if (toInjectAtStart.length() != 0) {
            String css = toInjectAtStart.join("");
            maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheetAtStart(css);
            if (toInjectAtStart == which) {
                toReturn = maybeReturn;
            }
            toInjectAtStart.setLength(0);
        }
        if (toInject.length() != 0) {
            String css = toInject.join("");
            maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheet(css);
            if (toInject == which) {
                toReturn = maybeReturn;
            }
            toInject.setLength(0);
        }
        if (toInjectAtEnd.length() != 0) {
            String css = toInjectAtEnd.join("");
            maybeReturn = StyleInjectorImpl.IMPL.injectStyleSheetAtEnd(css);
            if (toInjectAtEnd == which) {
                toReturn = maybeReturn;
            }
            toInjectAtEnd.setLength(0);
        }
        needsInjection = false;
        return toReturn;
    }

    private static void inject(boolean immediate) {
        if (immediate) {
            flush(null);
        } else {
            schedule();
        }
    }

    private static void schedule() {
        if (!needsInjection) {
            needsInjection = true;
            Scheduler.get().scheduleFinally(flusher);
        }
    }

    /**
     * Utility class.
     */
    private StyleInjector() {
    }

    /**
     * The DOM-compatible way of adding stylesheets. This implementation
     * requires the host HTML page to have a head element defined.
     */
    public static class StyleInjectorImpl {
        private static final StyleInjectorImpl IMPL = GWT.create(StyleInjectorImpl.class);

        private HeadElement head;

        public StyleElement injectStyleSheet(String contents) {
            StyleElement style = createElement(contents);
            getHead().appendChild(style);
            return style;
        }

        public StyleElement injectStyleSheetAtEnd(String contents) {
            return injectStyleSheet(contents);
        }

        public StyleElement injectStyleSheetAtStart(String contents) {
            StyleElement style = createElement(contents);
            getHead().insertBefore(style, head.getFirstChild());
            return style;
        }

        public void setContents(StyleElement style, String contents) {
            style.setInnerText(contents);
        }

        private StyleElement createElement(String contents) {
            StyleElement style = Document.get().createStyleElement();
            style.setPropertyString("language", "text/css");
            setContents(style, contents);
            return style;
        }

        private HeadElement getHead() {
            if (head == null) {
                Element elt = Document.get().getElementsByTagName("head").getItem(0);
                assert elt != null : "The host HTML page does not have a <head> element"
                        + " which is required by StyleInjector";
                head = HeadElement.as(elt);
            }
            return head;
        }
    }

    /**
     * IE doesn't allow manipulation of a style element through DOM methods.
     * There is also a hard-coded limit on the number of times that
     * createStyleSheet can be called before IE8-9 starts throwing exceptions.
     */
    public static class StyleInjectorImplIE extends StyleInjectorImpl {
        /**
         * The maximum number of style tags that can be handled by IE.
         */
        private static final int MAX_STYLE_SHEETS = 31;

        /**
         * A cache of the lengths of the current style sheets. A value of 0
         * indicates that the length has not yet been retrieved.
         */
        private static int[] styleSheetLengths = new int[MAX_STYLE_SHEETS];

        private static native int getDocumentStyleCount() /*-{
                                                          var count = 0;
                                                          for (var idx = 0; idx < $doc.styleSheets.length; idx++) {
                                                          var sheet = $doc.styleSheets[idx];
                                                          if (sheet.owningElement.tagName.toLowerCase() == 'style') {
                                                          count++;
                                                          }
                                                          }
                                                          return count;
                                                          }-*/;

        private static native int getDocumentStyleSheetLength(int index) /*-{
                                                                         var remote = @com.google.gwt.dom.client.StyleInjector.StyleInjectorImplIE::getDocumentStyleSheet(I)(index);
                                                                         return remote.sheet.cssText.length;
                                                                         }-*/;

        static native StyleElement getDocumentStyleSheet(int index) /*-{
                                                                    for (var idx = 0; idx < $doc.styleSheets.length; idx++) {
                                                                    var sheet = $doc.styleSheets[idx];
                                                                    if (sheet.owningElement.tagName.toLowerCase() == 'style') {
                                                                    if (index-- == 0) {
                                                                    var remote = sheet.owningElement;
                                                                    return @com.google.gwt.dom.client.LocalDom::nodeFor(Lcom/google/gwt/core/client/JavaScriptObject;)(remote);
                                                                    }
                                                                    }
                                                                    }
                                                                    return null;
                                                                    }-*/;

        private boolean injectedOnce = false;

        public native void appendContents(StyleElement style, String contents) /*-{
                                                                               style.@com.google.gwt.dom.client.Element::ensureRemote()().sheet.cssText += contents;
                                                                               }-*/;

        @Override
        public StyleElement injectStyleSheet(String contents) {
            int numStyles = getDocumentStyleCount();
            if (numStyles < MAX_STYLE_SHEETS) {
                // Just create a new style element and add it to the list
                return createNewStyleSheet(contents);
            } else {
                /*
                 * Find shortest style element to minimize re-parse time in the
                 * general case.
                 * 
                 * We cache the lengths of the style sheets in order to avoid
                 * expensive calls to retrieve their actual contents. Note that
                 * if another module or script makes changes to the style sheets
                 * that we are unaware of, the worst that will happen is that we
                 * will choose a style sheet to append to that is not actually
                 * of minimum size.
                 *
                 * We also play safe by counting only the MAX_STYLE_SHEETS first
                 * style sheets, just in case the limits are raised somehow
                 * (e.g. if this implementation is used in IE10 which removes
                 * --or significantly raises-- the limits.)
                 */
                int shortestLen = Integer.MAX_VALUE;
                int shortestIdx = -1;
                for (int i = 0; i < MAX_STYLE_SHEETS; i++) {
                    int len = styleSheetLengths[i];
                    if (len == 0) {
                        // Cache the length
                        len = styleSheetLengths[i] = getDocumentStyleSheetLength(i);
                    }
                    if (len <= shortestLen) {
                        shortestLen = len;
                        shortestIdx = i;
                    }
                }
                styleSheetLengths[shortestIdx] += contents.length();
                return appendToStyleSheet(shortestIdx, contents, true);
            }
        }

        @Override
        public StyleElement injectStyleSheetAtEnd(String contents) {
            int documentStyleCount = getDocumentStyleCount();
            if (documentStyleCount == 0) {
                return createNewStyleSheet(contents);
            }
            return appendToStyleSheet(documentStyleCount - 1, contents, true);
        }

        @Override
        public StyleElement injectStyleSheetAtStart(String contents) {
            if (getDocumentStyleCount() == 0) {
                return createNewStyleSheet(contents);
            }
            return appendToStyleSheet(0, contents, false); // prepend
        }

        public native void prependContents(StyleElement style, String contents) /*-{
                                                                                style.@com.google.gwt.dom.client.Element::ensureRemote()().sheet.cssText = contents + style.sheet.cssText;
                                                                                }-*/;

        public native void setContents(StyleElement style, String contents) /*-{
                                                                            debugger;
                                                                            var elem = style.@com.google.gwt.dom.client.Element::ensureRemote()();
                                                                            elem.sheet.cssText = contents ;
                                                                            }-*/;

        private StyleElement appendToStyleSheet(int idx, String contents, boolean append) {
            StyleElement style = getDocumentStyleSheet(idx);
            if (append) {
                appendContents(style, contents);
            } else {
                prependContents(style, contents);
            }
            return style;
        }

        private StyleElement created;

        private StyleElement createNewStyleSheet(String contents) {
            StyleElement styleElement = Document.get().createStyleElement();
            styleElement.setInnerText(contents);
            ElementRemote remote = createNewStyleSheet0(contents);
            //set remote before attach - head will already have been flushed
            styleElement.implAccess().setRemote(remote);
            Document.get().getHead().implAccess().appendChildLocalOnly(styleElement);
            return styleElement;

        }

        private native ElementRemote createNewStyleSheet0(String contents) /*-{
                                                                           var sheet = $doc.createStyleSheet();
                                                                           sheet.cssText=contents;
                                                                           return sheet.owningElement;
                                                                           }-*/;
    }
}