| /* |
| * 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} — 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 { |
| |
| /** |
| * 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() /*-{ |
| return $doc.styleSheets.length; |
| }-*/; |
| |
| private static native StyleElement getDocumentStyleSheet(int index) /*-{ |
| return $doc.styleSheets[index]; |
| }-*/; |
| |
| private static native int getDocumentStyleSheetLength(int index) /*-{ |
| return $doc.styleSheets[index].cssText.length; |
| }-*/; |
| |
| public native void appendContents(StyleElement style, String contents) /*-{ |
| style.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.cssText = contents + style.cssText; |
| }-*/; |
| |
| 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 native StyleElement createElement() /*-{ |
| return $doc.createStyleSheet(); |
| }-*/; |
| |
| private StyleElement createNewStyleSheet(String contents) { |
| StyleElement style = createElement(); |
| style.setCssText(contents); |
| return style; |
| } |
| } |
| |
| 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() { |
| } |
| } |