com.google.gwt.dom.client.StyleInjector Maven / Gradle / Ivy
/*
* 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.
*
* The api here is a bit redundant, with similarly named methods returning
* either void
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 IE developers should
* avoid the methods with {@link StyleElement} return values (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
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.
*
* 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.
*
* 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 true
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 true
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 true
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.
*
* The returned StyleElement cannot be implemented consistently across all
* browsers. Specifically, applications that need to run on Internet
* Explorer should not use this method. Call {@link #inject(String)}
* instead.
*
* @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)}.
*
* The returned StyleElement cannot be implemented consistently across all
* browsers. Specifically, applications that need to run on Internet
* Explorer should not use this method. Call {@link #injectAtEnd(String)}
* instead.
*
* @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)}.
*
* The returned StyleElement cannot be implemented consistently across all
* browsers. Specifically, applications that need to run on Internet
* Explorer should not use this method. Call {@link #injectAtStart(String, boolean)}
* instead.
*
* @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.
*
* This method should be used with some caution as StyleInjector may recycle
* StyleElements on certain browsers. Specifically, applications that
* need to run on Internet Explorer should not use this method.
*
* @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 which
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() {
}
}