зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central. a=merge
This commit is contained in:
Коммит
f3c2b2ecf8
|
@ -33,6 +33,9 @@ const PREF_TEST_ROOT = "mochitest.testRoot";
|
|||
const PREF_ENABLED = "browser.policies.enabled";
|
||||
const PREF_LOGLEVEL = "browser.policies.loglevel";
|
||||
|
||||
// To force disallowing enterprise-only policies during tests
|
||||
const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
|
@ -127,6 +130,11 @@ EnterprisePoliciesManager.prototype = {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
|
||||
log.error(`Policy ${policyName} is only allowed on ESR`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let [parametersAreValid, parsedParameters] =
|
||||
PoliciesValidator.validateAndParseParameters(policyParameters,
|
||||
policySchema);
|
||||
|
@ -305,6 +313,33 @@ EnterprisePoliciesManager.prototype = {
|
|||
|
||||
let DisallowedFeatures = {};
|
||||
|
||||
/**
|
||||
* areEnterpriseOnlyPoliciesAllowed
|
||||
*
|
||||
* Checks whether the policies marked as enterprise_only in the
|
||||
* schema are allowed to run on this browser.
|
||||
*
|
||||
* This is meant to only allow policies to run on ESR, but in practice
|
||||
* we allow it to run on channels different than release, to allow
|
||||
* these policies to be tested on pre-release channels.
|
||||
*
|
||||
* @returns {Bool} Whether the policy can run.
|
||||
*/
|
||||
function areEnterpriseOnlyPoliciesAllowed() {
|
||||
if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
|
||||
// This is used as an override to test the "enterprise_only"
|
||||
// functionality itself on tests, which would always return
|
||||
// true due to the Cu.isInAutomation check below.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AppConstants.MOZ_UPDATE_CHANNEL != "release" ||
|
||||
Cu.isInAutomation) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* JSON PROVIDER OF POLICIES
|
||||
|
|
|
@ -7,6 +7,7 @@ support-files =
|
|||
config_broken_json.json
|
||||
|
||||
[browser_policies_broken_json.js]
|
||||
[browser_policies_enterprise_only.js]
|
||||
[browser_policies_notice_in_aboutpreferences.js]
|
||||
[browser_policies_popups_cookies_addons_flash.js]
|
||||
[browser_policies_runOnce_helper.js]
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
|
||||
|
||||
add_task(async function test_enterprise_only_policies() {
|
||||
let { Policies } = ChromeUtils.import("resource:///modules/policies/Policies.jsm", {});
|
||||
|
||||
let normalPolicyRan = false, enterprisePolicyRan = false;
|
||||
|
||||
Policies.NormalPolicy = {
|
||||
onProfileAfterChange(manager, param) {
|
||||
normalPolicyRan = true;
|
||||
}
|
||||
};
|
||||
|
||||
Policies.EnterpriseOnlyPolicy = {
|
||||
onProfileAfterChange(manager, param) {
|
||||
enterprisePolicyRan = true;
|
||||
}
|
||||
};
|
||||
|
||||
Services.prefs.setBoolPref(PREF_DISALLOW_ENTERPRISE, true);
|
||||
|
||||
await setupPolicyEngineWithJson(
|
||||
// policies.json
|
||||
{
|
||||
"policies": {
|
||||
"NormalPolicy": true,
|
||||
"EnterpriseOnlyPolicy": true
|
||||
}
|
||||
},
|
||||
|
||||
// custom schema
|
||||
{
|
||||
properties: {
|
||||
"NormalPolicy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
"EnterpriseOnlyPolicy": {
|
||||
"type": "boolean",
|
||||
"enterprise_only": true
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
|
||||
is(normalPolicyRan, true, "Normal policy ran as expected");
|
||||
is(enterprisePolicyRan, false, "Enterprise-only policy was prevented from running");
|
||||
|
||||
// Clean-up
|
||||
delete Policies.NormalPolicy;
|
||||
delete Policies.EnterpriseOnlyPolicy;
|
||||
Services.prefs.clearUserPref(PREF_DISALLOW_ENTERPRISE);
|
||||
});
|
|
@ -16,6 +16,5 @@ support-files =
|
|||
[chrome/test_cssanimation_missing_keyframes.html]
|
||||
[chrome/test_generated_content_getAnimations.html]
|
||||
[chrome/test_running_on_compositor.html]
|
||||
skip-if = os == 'android' # bug 1442150
|
||||
[chrome/test_simulate_compute_values_failure.html]
|
||||
skip-if = !debug
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -96,16 +96,6 @@ function waitForWheelEvent(aTarget) {
|
|||
});
|
||||
}
|
||||
|
||||
// Returns true if |aAnimation| begins at the current timeline time. We
|
||||
// sometimes need to detect this case because if we started an animation
|
||||
// asynchronously (e.g. using play()) and then ended up running the next frame
|
||||
// at precisely the time the animation started (due to aligning with vsync
|
||||
// refresh rate) then we won't end up restyling in that frame.
|
||||
function startsRightNow(aAnimation) {
|
||||
return aAnimation.startTime === aAnimation.timeline.currentTime &&
|
||||
aAnimation.currentTime === 0;
|
||||
}
|
||||
|
||||
function tweakExpectedRestyleCount(aAnimation, aExpectedRestyleCount) {
|
||||
// Normally we expect one restyling for each requestAnimationFrame (as
|
||||
// called by observeRestyling) PLUS one for the last frame because of bug
|
||||
|
@ -123,10 +113,10 @@ function tweakExpectedRestyleCount(aAnimation, aExpectedRestyleCount) {
|
|||
// If we have the conformant Promise handling and |aAnimation| begins at
|
||||
// the current timeline time, we will not process restyling in the initial
|
||||
// frame.
|
||||
if (startsRightNow(aAnimation)) {
|
||||
if (animationStartsRightNow(aAnimation)) {
|
||||
return aExpectedRestyleCount - 1;
|
||||
}
|
||||
} else if (!startsRightNow(aAnimation)) {
|
||||
} else if (!animationStartsRightNow(aAnimation)) {
|
||||
// If we don't have the conformant Promise handling and |aAnimation|
|
||||
// doesn't begin at the current timeline time, we will see an additional
|
||||
// restyling in the last frame.
|
||||
|
|
|
@ -419,3 +419,14 @@ function waitForPaints() {
|
|||
// (bug 1341294).
|
||||
return waitForAnimationFrames(2);
|
||||
}
|
||||
|
||||
// Returns true if |aAnimation| begins at the current timeline time. We
|
||||
// sometimes need to detect this case because if we started an animation
|
||||
// asynchronously (e.g. using play()) and then ended up running the next frame
|
||||
// at precisely the time the animation started (due to aligning with vsync
|
||||
// refresh rate) then we won't end up restyling in that frame.
|
||||
function animationStartsRightNow(aAnimation) {
|
||||
return aAnimation.startTime === aAnimation.timeline.currentTime &&
|
||||
aAnimation.currentTime === 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -5089,6 +5089,13 @@ EditorBase::FindSelectionRoot(nsINode* aNode)
|
|||
return rootContent.forget();
|
||||
}
|
||||
|
||||
void
|
||||
EditorBase::InitializeSelectionAncestorLimit(Selection& aSelection,
|
||||
nsIContent& aAncestorLimit)
|
||||
{
|
||||
aSelection.SetAncestorLimiter(&aAncestorLimit);
|
||||
}
|
||||
|
||||
nsresult
|
||||
EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
|
||||
{
|
||||
|
@ -5099,10 +5106,6 @@ EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
bool isTargetDoc =
|
||||
targetNode->NodeType() == nsINode::DOCUMENT_NODE &&
|
||||
targetNode->HasFlag(NODE_IS_EDITABLE);
|
||||
|
||||
RefPtr<Selection> selection = GetSelection();
|
||||
NS_ENSURE_STATE(selection);
|
||||
|
||||
|
@ -5130,24 +5133,18 @@ EditorBase::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
|
|||
nsISelectionDisplay::DISPLAY_ALL);
|
||||
selectionController->RepaintSelection(
|
||||
nsISelectionController::SELECTION_NORMAL);
|
||||
|
||||
// If the computed selection root isn't root content, we should set it
|
||||
// as selection ancestor limit. However, if that is root element, it means
|
||||
// there is not limitation of the selection, then, we must set nullptr.
|
||||
// NOTE: If we set a root element to the ancestor limit, some selection
|
||||
// methods don't work fine.
|
||||
if (selectionRootContent->GetParent()) {
|
||||
selection->SetAncestorLimiter(selectionRootContent);
|
||||
InitializeSelectionAncestorLimit(*selection, *selectionRootContent);
|
||||
} else {
|
||||
selection->SetAncestorLimiter(nullptr);
|
||||
}
|
||||
|
||||
// XXX What case needs this?
|
||||
if (isTargetDoc) {
|
||||
if (!selection->RangeCount()) {
|
||||
BeginningOfDocument();
|
||||
}
|
||||
}
|
||||
|
||||
// If there is composition when this is called, we may need to restore IME
|
||||
// selection because if the editor is reframed, this already forgot IME
|
||||
// selection and the transaction.
|
||||
|
|
|
@ -733,6 +733,19 @@ protected:
|
|||
void BeginPlaceholderTransaction(nsAtom* aTransactionName);
|
||||
void EndPlaceholderTransaction();
|
||||
|
||||
/**
|
||||
* InitializeSelectionAncestorLimit() is called by InitializeSelection().
|
||||
* When this is called, each implementation has to call
|
||||
* aSelection.SetAncestorLimiter() with aAnotherLimit.
|
||||
*
|
||||
* @param aSelection The selection.
|
||||
* @param aAncestorLimit New ancestor limit of aSelection. This always
|
||||
* has parent node. So, it's always safe to
|
||||
* call SetAncestorLimit() with this node.
|
||||
*/
|
||||
virtual void InitializeSelectionAncestorLimit(Selection& aSelection,
|
||||
nsIContent& aAncestorLimit);
|
||||
|
||||
public:
|
||||
/**
|
||||
* All editor operations which alter the doc should be prefaced
|
||||
|
|
|
@ -507,81 +507,173 @@ HTMLEditor::InitRules()
|
|||
|
||||
NS_IMETHODIMP
|
||||
HTMLEditor::BeginningOfDocument()
|
||||
{
|
||||
return MaybeCollapseSelectionAtFirstEditableNode(false);
|
||||
}
|
||||
|
||||
void
|
||||
HTMLEditor::InitializeSelectionAncestorLimit(Selection& aSelection,
|
||||
nsIContent& aAncestorLimit)
|
||||
{
|
||||
// Hack for initializing selection.
|
||||
// HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode() will try to
|
||||
// collapse selection at first editable text node or inline element which
|
||||
// cannot have text nodes as its children. However, selection has already
|
||||
// set into the new editing host by user, we should not change it. For
|
||||
// solving this issue, we should do nothing if selection range is in active
|
||||
// editing host except it's not collapsed at start of the editing host since
|
||||
// aSelection.SetAncestorLimiter(aAncestorLimit) will collapse selection
|
||||
// at start of the new limiter if focus node of aSelection is outside of the
|
||||
// editing host. However, we need to check here if selection is already
|
||||
// collapsed at start of the editing host because it's possible JS to do it.
|
||||
// In such case, we should not modify selection with calling
|
||||
// MaybeCollapseSelectionAtFirstEditableNode().
|
||||
|
||||
// Basically, we should try to collapse selection at first editable node
|
||||
// in HTMLEditor.
|
||||
bool tryToCollapseSelectionAtFirstEditableNode = true;
|
||||
if (aSelection.RangeCount() == 1 && aSelection.IsCollapsed()) {
|
||||
Element* editingHost = GetActiveEditingHost();
|
||||
nsRange* range = aSelection.GetRangeAt(0);
|
||||
if (range->GetStartContainer() == editingHost &&
|
||||
!range->StartOffset()) {
|
||||
// JS or user operation has already collapsed selection at start of
|
||||
// the editing host. So, we don't need to try to change selection
|
||||
// in this case.
|
||||
tryToCollapseSelectionAtFirstEditableNode = false;
|
||||
}
|
||||
}
|
||||
|
||||
EditorBase::InitializeSelectionAncestorLimit(aSelection, aAncestorLimit);
|
||||
|
||||
// XXX Do we need to check if we still need to change selection? E.g.,
|
||||
// we could have already lost focus while we're changing the ancestor
|
||||
// limiter because it may causes "selectionchange" event.
|
||||
if (tryToCollapseSelectionAtFirstEditableNode) {
|
||||
MaybeCollapseSelectionAtFirstEditableNode(true);
|
||||
}
|
||||
}
|
||||
|
||||
nsresult
|
||||
HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
|
||||
bool aIgnoreIfSelectionInEditingHost)
|
||||
{
|
||||
// XXX Why doesn't this check if the document is alive?
|
||||
if (!IsInitialized()) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
// Get the selection
|
||||
RefPtr<Selection> selection = GetSelection();
|
||||
NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
|
||||
if (NS_WARN_IF(!selection)) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
// Get the root element.
|
||||
nsCOMPtr<Element> rootElement = GetRoot();
|
||||
if (!rootElement) {
|
||||
NS_WARNING("GetRoot() returned a null pointer (mRootElement is null)");
|
||||
// Use editing host. If you use root element here, selection may be
|
||||
// moved to <head> element, e.g., if there is a text node in <script>
|
||||
// element. So, we should use active editing host.
|
||||
RefPtr<Element> editingHost = GetActiveEditingHost();
|
||||
if (NS_WARN_IF(!editingHost)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Find first editable thingy
|
||||
bool done = false;
|
||||
nsCOMPtr<nsINode> curNode = rootElement.get(), selNode;
|
||||
int32_t curOffset = 0, selOffset = 0;
|
||||
while (!done) {
|
||||
WSRunObject wsObj(this, curNode, curOffset);
|
||||
// If selection range is already in the editing host and the range is not
|
||||
// start of the editing host, we shouldn't reset selection. E.g., window
|
||||
// is activated when the editor had focus before inactivated.
|
||||
if (aIgnoreIfSelectionInEditingHost && selection->RangeCount() == 1) {
|
||||
nsRange* range = selection->GetRangeAt(0);
|
||||
if (!range->Collapsed() ||
|
||||
range->GetStartContainer() != editingHost.get() ||
|
||||
range->StartOffset()) {
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// Find first editable and visible node.
|
||||
EditorRawDOMPoint pointToPutCaret(editingHost, 0);
|
||||
for (;;) {
|
||||
WSRunObject wsObj(this, pointToPutCaret.GetContainer(),
|
||||
pointToPutCaret.Offset());
|
||||
int32_t visOffset = 0;
|
||||
WSType visType;
|
||||
nsCOMPtr<nsINode> visNode;
|
||||
wsObj.NextVisibleNode(curNode, curOffset, address_of(visNode), &visOffset,
|
||||
&visType);
|
||||
if (visType == WSType::normalWS || visType == WSType::text) {
|
||||
selNode = visNode;
|
||||
selOffset = visOffset;
|
||||
done = true;
|
||||
} else if (visType == WSType::br || visType == WSType::special) {
|
||||
selNode = visNode->GetParentNode();
|
||||
selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
|
||||
done = true;
|
||||
} else if (visType == WSType::otherBlock) {
|
||||
// By definition of WSRunObject, a block element terminates a
|
||||
// whitespace run. That is, although we are calling a method that is
|
||||
// named "NextVisibleNode", the node returned might not be
|
||||
// visible/editable!
|
||||
//
|
||||
// If the given block does not contain any visible/editable items, we
|
||||
// want to skip it and continue our search.
|
||||
wsObj.NextVisibleNode(pointToPutCaret.GetContainer(),
|
||||
pointToPutCaret.Offset(),
|
||||
address_of(visNode), &visOffset, &visType);
|
||||
|
||||
if (!IsContainer(visNode)) {
|
||||
// However, we were given a block that is not a container. Since the
|
||||
// block can not contain anything that's visible, such a block only
|
||||
// makes sense if it is visible by itself, like a <hr>. We want to
|
||||
// place the caret in front of that block.
|
||||
selNode = visNode->GetParentNode();
|
||||
selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
|
||||
done = true;
|
||||
} else {
|
||||
bool isEmptyBlock;
|
||||
if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) &&
|
||||
isEmptyBlock) {
|
||||
// Skip the empty block
|
||||
curNode = visNode->GetParentNode();
|
||||
curOffset = curNode ? curNode->ComputeIndexOf(visNode) : -1;
|
||||
curOffset++;
|
||||
} else {
|
||||
curNode = visNode;
|
||||
curOffset = 0;
|
||||
}
|
||||
// Keep looping
|
||||
}
|
||||
// If we meet a non-editable node first, we should move caret to start of
|
||||
// the editing host (perhaps, user may want to insert something before
|
||||
// the first non-editable node? Chromium behaves so).
|
||||
if (visNode && !visNode->IsEditable()) {
|
||||
pointToPutCaret.Set(editingHost, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// WSRunObject::NextVisibleNode() returns WSType::special and the "special"
|
||||
// node when it meets empty inline element. In this case, we should go to
|
||||
// next sibling. For example, if current editor is:
|
||||
// <div contenteditable><span></span><b><br></b></div>
|
||||
// then, we should put caret at the <br> element. So, let's check if
|
||||
// found node is an empty inline container element.
|
||||
if (visType == WSType::special && visNode &&
|
||||
TagCanContainTag(*visNode->NodeInfo()->NameAtom(),
|
||||
*nsGkAtoms::textTagName)) {
|
||||
pointToPutCaret.Set(visNode);
|
||||
DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
|
||||
NS_WARNING_ASSERTION(advanced,
|
||||
"Failed to advance offset from found empty inline container element");
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is editable and visible text node, move caret at start of it.
|
||||
if (visType == WSType::normalWS || visType == WSType::text) {
|
||||
pointToPutCaret.Set(visNode, visOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is editable <br> or something inline special element like
|
||||
// <img>, <input>, etc, move caret before it.
|
||||
if (visType == WSType::br || visType == WSType::special) {
|
||||
pointToPutCaret.Set(visNode);
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is no visible/editable node except another block element in
|
||||
// current editing host, we should move caret to very first of the editing
|
||||
// host.
|
||||
// XXX This may not make sense, but Chromium behaves so. Therefore, the
|
||||
// reason why we do this is just compatibility with Chromium.
|
||||
if (visType != WSType::otherBlock) {
|
||||
pointToPutCaret.Set(editingHost, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// By definition of WSRunObject, a block element terminates a whitespace
|
||||
// run. That is, although we are calling a method that is named
|
||||
// "NextVisibleNode", the node returned might not be visible/editable!
|
||||
|
||||
// However, we were given a block that is not a container. Since the
|
||||
// block can not contain anything that's visible, such a block only
|
||||
// makes sense if it is visible by itself, like a <hr>. We want to
|
||||
// place the caret in front of that block.
|
||||
if (!IsContainer(visNode)) {
|
||||
pointToPutCaret.Set(visNode);
|
||||
break;
|
||||
}
|
||||
|
||||
// If the given block does not contain any visible/editable items, we want
|
||||
// to skip it and continue our search.
|
||||
bool isEmptyBlock;
|
||||
if (NS_SUCCEEDED(IsEmptyNode(visNode, &isEmptyBlock)) && isEmptyBlock) {
|
||||
// Skip the empty block
|
||||
pointToPutCaret.Set(visNode);
|
||||
DebugOnly<bool> advanced = pointToPutCaret.AdvanceOffset();
|
||||
NS_WARNING_ASSERTION(advanced,
|
||||
"Failed to advance offset from the found empty block node");
|
||||
} else {
|
||||
// Else we found nothing useful
|
||||
selNode = curNode;
|
||||
selOffset = curOffset;
|
||||
done = true;
|
||||
pointToPutCaret.Set(visNode, 0);
|
||||
}
|
||||
}
|
||||
return selection->Collapse(selNode, selOffset);
|
||||
return selection->Collapse(pointToPutCaret);
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
|
|
@ -327,6 +327,10 @@ protected:
|
|||
using EditorBase::IsBlockNode;
|
||||
virtual bool IsBlockNode(nsINode *aNode) override;
|
||||
|
||||
virtual void
|
||||
InitializeSelectionAncestorLimit(Selection& aSelection,
|
||||
nsIContent& aAncestorLimit) override;
|
||||
|
||||
public:
|
||||
// XXX Why don't we move following methods above for grouping by the origins?
|
||||
NS_IMETHOD SetFlags(uint32_t aFlags) override;
|
||||
|
@ -540,6 +544,32 @@ public:
|
|||
*/
|
||||
nsresult DoInlineTableEditingAction(const Element& aUIAnonymousElement);
|
||||
|
||||
/**
|
||||
* MaybeCollapseSelectionAtFirstEditableNode() may collapse selection at
|
||||
* proper position to staring to edit. If there is a non-editable node
|
||||
* before any editable text nodes or inline elements which can have text
|
||||
* nodes as their children, collapse selection at start of the editing
|
||||
* host. If there is an editable text node which is not collapsed, collapses
|
||||
* selection at the start of the text node. If there is an editable inline
|
||||
* element which cannot have text nodes as its child, collapses selection at
|
||||
* before the element node. Otherwise, collapses selection at start of the
|
||||
* editing host.
|
||||
*
|
||||
* @param aIgnoreIfSelectionInEditingHost
|
||||
* This method does nothing if selection is in the
|
||||
* editing host except if it's collapsed at start of
|
||||
* the editing host.
|
||||
* Note that if selection ranges were outside of
|
||||
* current selection limiter, selection was collapsed
|
||||
* at the start of the editing host therefore, if
|
||||
* you call this with setting this to true, you can
|
||||
* keep selection ranges if user has already been
|
||||
* changed.
|
||||
*/
|
||||
nsresult
|
||||
MaybeCollapseSelectionAtFirstEditableNode(
|
||||
bool aIgnoreIfSelectionInEditingHost);
|
||||
|
||||
protected:
|
||||
class BlobReader final : public nsIEditorBlobListener
|
||||
{
|
||||
|
|
|
@ -101,8 +101,8 @@ function runTestsInternal()
|
|||
var range = selection.getRangeAt(0);
|
||||
ok(range.collapsed, "the selection range isn't collapsed");
|
||||
var startNode = range.startContainer;
|
||||
is(startNode.nodeType, 1, "the caret isn't set to the div node");
|
||||
is(startNode, editor, "the caret isn't set to the editor");
|
||||
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
|
||||
is(startNode, editor.firstChild, "the caret isn't set to the editor");
|
||||
ok(selCon.caretVisible, "caret isn't visible in the editor");
|
||||
// Move focus to other editor
|
||||
otherEditor.focus();
|
||||
|
@ -113,8 +113,8 @@ function runTestsInternal()
|
|||
range = selection.getRangeAt(0);
|
||||
ok(range.collapsed, "the selection range isn't collapsed");
|
||||
var startNode = range.startContainer;
|
||||
is(startNode.nodeType, 1, "the caret isn't set to the div node");
|
||||
is(startNode, otherEditor, "the caret isn't set to the other editor");
|
||||
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the text node");
|
||||
is(startNode, otherEditor.firstChild, "the caret isn't set to the other editor");
|
||||
ok(selCon.caretVisible, "caret isn't visible in the other editor");
|
||||
// Move focus to inputTextInEditor
|
||||
inputTextInEditor.focus();
|
||||
|
@ -124,9 +124,9 @@ function runTestsInternal()
|
|||
range = selection.getRangeAt(0);
|
||||
ok(range.collapsed, "the selection range isn't collapsed");
|
||||
var startNode = range.startContainer;
|
||||
is(startNode.nodeType, 1, "the caret isn't set to the div node");
|
||||
is(startNode.nodeType, Node.TEXT_NODE, "the caret isn't set to the first text node");
|
||||
// XXX maybe, the caret can stay on the other editor if it's better.
|
||||
is(startNode, editor,
|
||||
is(startNode, editor.firstChild,
|
||||
"the caret should stay on the other editor");
|
||||
ok(selCon.caretVisible,
|
||||
"caret isn't visible in the inputTextInEditor");
|
||||
|
|
|
@ -330,7 +330,18 @@ interface nsIEditor : nsISupports
|
|||
/** sets the document selection to the entire contents of the document */
|
||||
void selectAll();
|
||||
|
||||
/** sets the document selection to the beginning of the document */
|
||||
/**
|
||||
* Collapses selection at start of the document. If it's an HTML editor,
|
||||
* collapses selection at start of current editing host (<body> element if
|
||||
* it's in designMode) instead. If there is a non-editable node before any
|
||||
* editable text nodes or inline elements which can have text nodes as their
|
||||
* children, collapses selection at start of the editing host. If there is
|
||||
* an editable text node which is not collapsed, collapses selection at
|
||||
* start of the text node. If there is an editable inline element which
|
||||
* cannot have text nodes as its child, collapses selection at before the
|
||||
* element node. Otherwise, collapses selection at start of the editing
|
||||
* host.
|
||||
*/
|
||||
void beginningOfDocument();
|
||||
|
||||
/** sets the document selection to the end of the document */
|
||||
|
|
|
@ -0,0 +1,683 @@
|
|||
.. role:: html(code)
|
||||
:language: html
|
||||
|
||||
.. role:: js(code)
|
||||
:language: javascript
|
||||
|
||||
=============================
|
||||
Fluent for Firefox Developers
|
||||
=============================
|
||||
|
||||
|
||||
This tutorial is intended for Firefox engineers already familiar with the previous
|
||||
localization systems offered by Gecko - `DTD`_ and `StringBundle`_ - and assumes
|
||||
prior experience with those systems.
|
||||
|
||||
|
||||
Using Fluent in Gecko
|
||||
=====================
|
||||
|
||||
`Fluent`_ is a modern localization system currently being progressively introduced into
|
||||
the Gecko platform with a focus on quality, performance, maintenance and completeness.
|
||||
|
||||
In order to ensure that Fluent is ready for engineers to work with, the initial
|
||||
migrations are performed manually with a lot of oversight from the involved
|
||||
stakeholders.
|
||||
|
||||
In this initial phase, `Firefox Preferences`_ is being migrated as the first target
|
||||
and as a result, the first bindings to be stabilized are for chrome-privileged
|
||||
XUL context.
|
||||
|
||||
From there we plan to focus on two areas:
|
||||
|
||||
- `Unprivileged Contexts`_
|
||||
- `System Add-ons`_
|
||||
|
||||
The end goal is replacing all uses of DTD and StringBundle within Firefox's codebase.
|
||||
|
||||
If you want to use Fluent and your code involves one of the areas currently unsupported,
|
||||
we'd like to work with you on getting Fluent ready for your code.
|
||||
|
||||
|
||||
Getting a Review
|
||||
----------------
|
||||
|
||||
If you end up working on any patch which touches FTL files, we have a temporary
|
||||
hook in place that will reject your patch unless you get an r+ from one of the following
|
||||
L10n Drivers:
|
||||
|
||||
- Francesco Lodolo (:flod)
|
||||
- Zibi Braniecki (:gandalf)
|
||||
- Axel Hecht (:pike)
|
||||
- Stas Malolepszy (:stas)
|
||||
|
||||
|
||||
Major Benefits
|
||||
==============
|
||||
|
||||
Not only was the previous system designed over 20 years ago using file formats
|
||||
never intended for localization, but also the Web stack which Fluent ties into has
|
||||
completely changed over the same period, and the domain of internationalization
|
||||
got a powerful foundation in the form of `Unicode`_, `CLDR`_ and `ICU`_ which Fluent tightly
|
||||
`interoperates with`__.
|
||||
|
||||
__ https://github.com/projectfluent/fluent/wiki/Fluent-and-Standards
|
||||
|
||||
While it is beyond the scope of this document to cover all the benefits of Fluent in detail,
|
||||
below is an attempt to select some most observable changes for each group of consumers.
|
||||
|
||||
|
||||
Developers
|
||||
----------
|
||||
|
||||
- Support for XUL, XHTML, HTML, Web Components, React, JS, Python and Rust
|
||||
- Strings are available in a single, unified localization context available for both DOM and runtime code
|
||||
- Full internationalization (i18n) support: date and time formatting, number formatting, plurals, genders etc.
|
||||
- Strong focus on `declarative API via DOM attributes`__
|
||||
- Extensible with custom formatters, Mozilla-specific APIs etc.
|
||||
- `Separation of concerns`__: localization details, and the added complexity of some languages, don't leak onto the source code and are no concern for developers
|
||||
- Compound messages link a single translation unit to a single UI element
|
||||
- `DOM Overlays`__ allow for localization of DOM fragments
|
||||
- Simplified build system model
|
||||
- No need for pre-processing instructions
|
||||
|
||||
__ https://github.com/projectfluent/fluent/wiki/Get-Started
|
||||
__ https://github.com/projectfluent/fluent/wiki/Design-Principles
|
||||
__ https://github.com/projectfluent/fluent.js/wiki/DOM-Overlays
|
||||
|
||||
|
||||
Product Quality
|
||||
------------------
|
||||
|
||||
- A robust, multilevel, `error fallback system`__ prevents XML errors and runtime errors
|
||||
- Simplified l10n API reduces the amount of l10n specific code and resulting bugs
|
||||
- Runtime localization allows for dynamic language changes and updates over-the-air
|
||||
- DOM Overlays increase localization security
|
||||
|
||||
Many other smaller improvements will be noticed by the users of the system over time
|
||||
and, with the new foundation, the Fluent team is `currently working`__ on multiple highly
|
||||
requested features which will further improve the experience of developing
|
||||
localizable UIs.
|
||||
|
||||
__ https://github.com/projectfluent/fluent/wiki/Error-Handling
|
||||
__ https://github.com/projectfluent/fluent/wiki/Roadmap
|
||||
|
||||
|
||||
Fluent Translation List - FTL
|
||||
=============================
|
||||
|
||||
Fluent introduces a new localization format designed specifically for easy readability
|
||||
and localization features offered by the system.
|
||||
|
||||
At first glance the format resembles `.properties` file. It may look like this:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
home-page-header = Home Page
|
||||
|
||||
# The label of a button opening a new tab
|
||||
new-tab-open = Open New Tab
|
||||
|
||||
But the FTL file format is significantly more powerful and the additional features
|
||||
quickly add up. In order to familiarize yourself with the basic features,
|
||||
consider reading through the `Fluent Syntax Guide`_ to understand
|
||||
a more complex example like:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
### These messages correspond to security and privacy user interface.
|
||||
###
|
||||
### Please, choose simple and non-threatening language when localizing
|
||||
### to help user feel in control when interacting with the UI.
|
||||
|
||||
## General Section
|
||||
|
||||
-brand-short-name = Firefox
|
||||
.gender = masculine
|
||||
|
||||
pref-pane =
|
||||
.title =
|
||||
{ PLATFORM() ->
|
||||
[windows] Options
|
||||
*[other] Preferences
|
||||
}
|
||||
.accesskey = C
|
||||
|
||||
# Variables:
|
||||
# $tabCount (Number) - number of container tabs to be closed
|
||||
containers-disable-alert-ok-button =
|
||||
{ $tabCount ->
|
||||
[one] Close { $tabCount } Container Tab
|
||||
*[other] Close { $tabCount } Container Tabs
|
||||
}
|
||||
|
||||
update-application-info =
|
||||
You are using { -brand-short-name } Version: { $version }.
|
||||
<span>Please, read the <a>privacy policy</a>.</span>
|
||||
|
||||
The above, of course, is a particular selection of complex strings intended to exemplify
|
||||
the new features and concepts introduced by Fluent.
|
||||
|
||||
In order to ensure the quality of the output, a lot of new checks and tooling
|
||||
has been added to the build system.
|
||||
`Pontoon`_, the main localization tool used to translate Firefox, has been rebuilding
|
||||
its user experience to support localizers in their work.
|
||||
|
||||
|
||||
Social Contract
|
||||
===============
|
||||
|
||||
Fluent uses the concept of a `social contract` between developer and localizers.
|
||||
This contract is established by the selection of a unique identifier, called :js:`l10n-id`,
|
||||
which carries a promise of being used in a particular place to carry a particular meaning.
|
||||
|
||||
The use of unique identifiers is not new for Firefox engineers, but it is important
|
||||
to recognize that Fluent formalizes this relationship.
|
||||
|
||||
.. important::
|
||||
|
||||
An important part of the contract is that the developer commits to treat the
|
||||
localization output as `opaque`. That means that no concatenations, replacements
|
||||
or splitting should happen after the translation is completed to generate the
|
||||
desired output.
|
||||
|
||||
In return, localizers enter the social contract by promising to provide an accurate
|
||||
and clean translation of the messages that match the request.
|
||||
|
||||
In previous localization systems, developers were responsible for differentiating
|
||||
string variant based on a platform via pre-processing instructions, or
|
||||
selecting which strings should be formatted using `PluralForms.jsm`.
|
||||
|
||||
In Fluent, the developer is not to be bothered with inner logic and complexity that the
|
||||
localization will use to construct the response. Whether `declensions`__ or other
|
||||
variant selection techniques are used is up to a localizer and their particular translation.
|
||||
From the developer perspective, Fluent returns a final string to be presented to
|
||||
the user, with no l10n logic required in the running code.
|
||||
|
||||
__ https://en.wikipedia.org/wiki/Declension
|
||||
|
||||
|
||||
Markup Localization
|
||||
===================
|
||||
|
||||
Fluent fully replaces the use of `DTD`_ in localization.
|
||||
|
||||
To localize an element in Fluent, the developer adds a new message to
|
||||
an FTL file and then has to associate an :js:`l10n-id` with the element
|
||||
by defining a :js:`data-l10n-id` attribute:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<h1 data-l10n-id="home-page-header" />
|
||||
|
||||
<button data-l10n-id="pref-pane" />
|
||||
|
||||
Fluent will take care of the rest, populating the element with the message value
|
||||
in its content and all localizable attributes if defined.
|
||||
|
||||
The difference compared to the use of DTD is that the developer provides only a single
|
||||
message to localize the whole element, rather than a separate entity for
|
||||
the value and each of the attributes.
|
||||
|
||||
The other change is that the developer can localize a whole fragment of DOM:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
|
||||
<span class="bold">
|
||||
<a href="http://www.mozilla.org/privacy" />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
-brand-short-name = Firefox
|
||||
update-application-info =
|
||||
You are using { -brand-short-name } Version: { $version }.
|
||||
<span>Please, read the <a>privacy policy</a>.</span>
|
||||
|
||||
|
||||
Fluent will overlay the translation onto the source fragment preserving attributes like
|
||||
:code:`class` and :code:`href` from the source and adding translations for the elements
|
||||
inside. The end result will look like this:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
|
||||
You are using Firefox Version: 60.0.
|
||||
<span class="bold">
|
||||
Please, read the <a href="http://www.mozilla.org/privacy">privacy policy</a>.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
This operation is sanitized, and Fluent takes care of selecting which elements and
|
||||
attributes can be safely provided by the localization.
|
||||
The list of allowed elements and attributes is `maintained by the W3C`__, and if
|
||||
the developer needs to allow for localization of additional attributes, they can
|
||||
whitelist them using :code:`data-l10n-attrs` list:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<label data-l10n-id="search-input" data-l10n-attrs="style" />
|
||||
|
||||
The above example adds an attribute :code:`style` to be allowed on this
|
||||
particular :code:`label` element.
|
||||
|
||||
|
||||
External Arguments
|
||||
------------------
|
||||
|
||||
Notice in the previous example the attribute :code:`data-l10n-args`, which is
|
||||
a JSON object storing variables exposed by the developer to the localizer.
|
||||
|
||||
This is the main channel for the developer to provide additional variables
|
||||
to be used in the localization.
|
||||
|
||||
It is very rare that the arguments are needed for localizations which previously
|
||||
used DTD, because such variables will usually have to be computed from the runtime code,
|
||||
but it is worth understanding that when the :code:`l10n-args` are set in
|
||||
the runtime code, they are in fact encoded via JSON and stored together with
|
||||
:code:`l10n-id` as an attribute on the element.
|
||||
|
||||
__ https://www.w3.org/TR/2011/WD-html5-20110525/text-level-semantics.html
|
||||
|
||||
|
||||
Runtime Localization
|
||||
====================
|
||||
|
||||
Fluent fully replaces the use of `StringBundle`_ in localization.
|
||||
|
||||
In almost every case the JS runtime code will operate on a particular document, either
|
||||
XUL, XHTML or HTML.
|
||||
|
||||
If the document has its markup already localized, then Fluent exposes a new
|
||||
attribute on the :js:`document` element - :js:`document.l10n`.
|
||||
|
||||
This property is an object of type :js:`DOMLocalization` which maintains the main
|
||||
localization context for this document and exposes it to runtime code as well.
|
||||
|
||||
With a focus on `declarative localization`__, the primary method of localization is
|
||||
to alter the localization attributes in the DOM. Fluent provides a method to facilitate this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.l10n.setAttributes(element, "new-panel-header");
|
||||
|
||||
This will set the :code:`data-l10n-id` on the element and translate it before the next
|
||||
animation frame.
|
||||
|
||||
The reason to use this API over manually setting the attribute is that it also
|
||||
facilitates encoding l10n arguments as JSON:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.l10n.setAttributes(element "containers-disable-alert-ok-button", {
|
||||
tabCount: 5
|
||||
}
|
||||
|
||||
__ https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers
|
||||
|
||||
|
||||
Non-Markup Localization
|
||||
-----------------------
|
||||
|
||||
In rare cases, when the runtime code needs to retrieve the translation and not
|
||||
apply it onto the DOM, Fluent provides an API to retrieve it:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let [ msg ] = await document.l10n.formatValues([
|
||||
["remove-containers-description"]
|
||||
]);
|
||||
|
||||
alert(msg);
|
||||
|
||||
This model is heavily discouraged and should be used only in cases where the
|
||||
DOM annotation is not possible.
|
||||
|
||||
.. note::
|
||||
|
||||
This API is currently only available as asynchronous. In case of Firefox,
|
||||
the only non-DOM localizable calls are used where the output goes to
|
||||
a third-party like Bluetooth, Notifications etc.
|
||||
All those cases should already be asynchronous.
|
||||
|
||||
|
||||
Internationalization
|
||||
====================
|
||||
|
||||
The majority of internationalization issues are implicitly handled by Fluent without
|
||||
any additional requirement. Full Unicode support, `bidirectionality`__, and
|
||||
correct number formatting work without any action required from either
|
||||
developer or localizer.
|
||||
|
||||
__ https://github.com/projectfluent/fluent/wiki/BiDi-in-Fluent
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.l10n.setAttributes(element, "welcome-message", {
|
||||
userName: "اليسع",
|
||||
count: 5
|
||||
});
|
||||
|
||||
A message like this localized to American English will correctly wrap the user
|
||||
name in directionality marks allowing the layout engine to determine how to
|
||||
display the bidirectional text.
|
||||
|
||||
On the other hand, the same message localized to Arabic will use the Eastern Arabic
|
||||
numeral for number "5".
|
||||
|
||||
|
||||
Plural Rules
|
||||
------------
|
||||
|
||||
The most common localization feature is the ability to provide different variants
|
||||
of the same string depending on plural categories.
|
||||
|
||||
Fluent replaces the use of the proprietary :code:`PluralForms.jsm` with a Unicode CLDR
|
||||
standard called `Plural Rules`_.
|
||||
|
||||
In order to allow localizers to use it, all the developer has to do is to pass
|
||||
an external argument number:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.l10n.setAttributes(element, "unread-warning", { unreadCount: 5 });
|
||||
|
||||
Localizers can use the argument to build a multi variant message if their
|
||||
language requires that:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
unread-warning =
|
||||
{ $unreadCount ->
|
||||
[one] You have { $unreadCount } unread message
|
||||
*[other] You have { $unreadCount } unread messages
|
||||
}
|
||||
|
||||
Fluent guesses that since the variant selection is performed based on a number,
|
||||
its `plural category`__ should be retrieved.
|
||||
|
||||
If the given translation doesn't need pluralization for the string (for example
|
||||
Japanese often will not), the localizer can replace it with:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
unread-warning = You have { $unreadCount } unread messages
|
||||
|
||||
and the message will preserve the social contract.
|
||||
|
||||
One additional feature is that the localizer can further improve the message by
|
||||
specifying variants for particular values:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
unread-warning =
|
||||
{ $unreadCount ->
|
||||
[0] You have no unread messages
|
||||
[1] You have one unread message
|
||||
*[other] You have { $unreadCount } unread messages
|
||||
}
|
||||
|
||||
The advantage here is that per-locale choices don't leak onto the source code
|
||||
and the developer is not affected.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
There is an important distinction between a variant keyed on plural category
|
||||
`one` and digit `1`. Although in English the two are synonymous, in other
|
||||
languages category `one` may be used for other numbers.
|
||||
For example in `Bosnian`__, category `one` is used for numbers like `1`, `21`, `31`
|
||||
and so on, and also for fractional numbers like `0.1`.
|
||||
|
||||
__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
||||
__ https://unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#bs
|
||||
|
||||
Partial Arguments
|
||||
-----------------
|
||||
|
||||
When it comes to formatting data, Fluent allows the developer to provide
|
||||
a set of parameters for the formatter, and the localizer can fine tune some of them.
|
||||
This technique is called `partial arguments`__.
|
||||
|
||||
For example, when formatting a date, the developer can just pass a JS :js:`Date` object,
|
||||
but its default formatting will be pretty expressive. In most cases, the developer
|
||||
may want to use some of the :js:`Intl.DateTimeFormat` options to select the default
|
||||
representation of the date in string:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
document.l10n.setAttributes(element, "welcome-message", {
|
||||
startDate: FluentDateTime(new Date(), {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
})
|
||||
});
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
welcome-message = Your session will start date: { $startDate }
|
||||
|
||||
In most cases, that will be enough and the date would get formatted in the current
|
||||
Firefox as `February 28, 2018`.
|
||||
|
||||
But if in some other locale the string would get too long, the localizer can fine
|
||||
tune the options as well:
|
||||
|
||||
.. code-block:: properties
|
||||
|
||||
welcome-message = Początek Twojej sesji: { DATETIME($startDate, month="short") }
|
||||
|
||||
This will adjust the length of the month token in the message to short and get formatted
|
||||
in Polish as `28 lut 2018`.
|
||||
|
||||
At the moment Fluent supports two formatters that match JS Intl API counterparts:
|
||||
|
||||
* **NUMBER**: `Intl.NumberFormat`__
|
||||
* **DATETIME**: `Intl.DateTimeFormat`__
|
||||
|
||||
With time more formatters will be added.
|
||||
|
||||
__ http://projectfluent.org/fluent/guide/functions.html#partial-arguments
|
||||
__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
|
||||
__ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
|
||||
|
||||
Registering New L10n Files
|
||||
==========================
|
||||
|
||||
In the previous system, a new localization file had to be registered in order to
|
||||
add it in the `jar.mn` file for packaging.
|
||||
|
||||
Fluent uses a wildcard statement packaging all localization resources into
|
||||
their component's `/localization/` directory.
|
||||
|
||||
That means that, if a new file is added to a component of Firefox already
|
||||
covered by Fluent like `browser`, it's enough to add the new file to the
|
||||
repository in a path like `browser/locales/en-US/browser/component/file.ftl` and
|
||||
the toolchain will package it into `browser/localization/browser/component/file.ftl`.
|
||||
|
||||
At runtime Firefox uses a special registry for all localization data. It will
|
||||
register the browser's `/localization/` directory and make all files inside it
|
||||
available to be references.
|
||||
|
||||
To make the document localized using Fluent, all the developer has to do is add
|
||||
a single polyfill for the Fluent API to the source and list the resources
|
||||
that will be used:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<link rel="localization" href="branding/brand.ftl"/>
|
||||
<link rel="localization" href="browser/preferences/preferences.ftl"/>
|
||||
<script src="chrome://global/content/l10n.js"></script>
|
||||
|
||||
For performance reasons the :html:`<link/>` elements have to be specified above the
|
||||
:html:`<script/>` and the :html:`<script/>` itself has to be synchronous in order to ensure
|
||||
that the localization happens before first paint.
|
||||
|
||||
This allows Fluent to trigger asynchronous resource loading early enough to
|
||||
perform the initial DOM translation before the initial layout.
|
||||
|
||||
The URI provided to the :html:`<link/>` element are relative paths within the localization
|
||||
system.
|
||||
|
||||
Notice that only the registration of the script is synchronous. All the I/O and
|
||||
translation happen asynchronously.
|
||||
|
||||
|
||||
Custom Contexts
|
||||
===============
|
||||
|
||||
The above method creates a single localization context per document.
|
||||
In almost all scenarios that's sufficient.
|
||||
|
||||
In rare edge cases where the developer needs to fetch additional resources, or
|
||||
the same resources in another language, it is possible to create additional
|
||||
contexts manually using `Localization` class:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
const { Localization } =
|
||||
ChromeUtils.import("resource://gre/modules/Localization.jsm", {});
|
||||
|
||||
|
||||
const myL10n = new Localization([
|
||||
"branding/brand.ftl",
|
||||
"browser/preferences/preferences.ftl"
|
||||
]);
|
||||
|
||||
|
||||
let [isDefaultMsg, isNotDefaultMsg] =
|
||||
myL10n.formatValues(["is-default", "is-not-default"]);
|
||||
|
||||
|
||||
.. admonition:: Example
|
||||
|
||||
An example of a use case is the Preferences UI in Firefox which uses the
|
||||
main context to localize the UI but also to build a search index.
|
||||
|
||||
It is common to build such search index both in a current langauge and additionally
|
||||
in English since a lot of documentation and online help exists in that language.
|
||||
|
||||
A developer may create manually a new context with the same resources as the main one
|
||||
uses, but hardcode it to `en-US` and then build the search index using both contexts.
|
||||
|
||||
Designing Localizable APIs
|
||||
==========================
|
||||
|
||||
When designing localizable APIs, the most important rule is to resolve localization as
|
||||
late as possible. That means that instead of resolving strings somewhere deep in the
|
||||
codebase and then passing them on or even caching, it is highly recommended to pass
|
||||
around :code:`l10n-id` or :code:`[l10n-id, l10n-args]` pairs until the top-most code
|
||||
resolves them or applies them onto the DOM element.
|
||||
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
When writing tests that involve both I18n and L10n, the general rule is that
|
||||
result strings are opaque. That means that the developer should not assume any particular
|
||||
value and should never test against it.
|
||||
|
||||
In case of raw i18n the :js:`resolvedOptions` method on all :js:`Intl.*` formatters
|
||||
makes it relatively easy. In case of localization, the recommended way is to test that
|
||||
the code sets the right :code:`l10n-id`/:code:`l10n-args` attributes like this:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
testedFunction();
|
||||
|
||||
const l10nAttrs = document.l10n.getAttributes(element);
|
||||
|
||||
deepEquals(l10nAttrs, {
|
||||
id: "my-expected-id",
|
||||
args: {
|
||||
unreadCount: 5
|
||||
}
|
||||
});
|
||||
|
||||
If the code really has to test for particular values in the localized UI, it is
|
||||
always better to scan for a variable:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
testedFunction();
|
||||
|
||||
equals(element.textContent.contains("John"));
|
||||
|
||||
.. important::
|
||||
|
||||
Testing against whole values is brittle and will break when we insert Unicode
|
||||
bidirectionality marks into the result string or adapt the output in other ways.
|
||||
|
||||
|
||||
Inner Structure of Fluent
|
||||
=========================
|
||||
|
||||
The inner structure of Fluent in Gecko is out of scope of this tutorial, but
|
||||
since the class and file names may show up during debugging or profiling,
|
||||
below is a list of major components, each with a corresponding file in `/intl/l10n`
|
||||
modules in Gecko.
|
||||
|
||||
|
||||
MessageContext
|
||||
--------------
|
||||
|
||||
MessageContext is the lowest level API. It's fully synchronous, contains a parser for the
|
||||
FTL file format and a resolver for the logic. It is not meant to be used by
|
||||
consumers directly.
|
||||
|
||||
In the future we intend to offer this layer for standardization and it may become
|
||||
part of the :js:`mozIntl.*` or even :js:`Intl.*` API sets.
|
||||
|
||||
That part of the codebase is also the first that we'll be looking to port to Rust.
|
||||
|
||||
|
||||
Localization
|
||||
------------
|
||||
|
||||
Localization is a higher level API which uses :js:`MessageContext` internally but
|
||||
provides a full layer of compound message formatting and robust error fall-backing.
|
||||
|
||||
It is intended for use in runtime code and contains all fundamental localization
|
||||
methods.
|
||||
|
||||
|
||||
DOMLocalization
|
||||
---------------
|
||||
|
||||
DOMLocalization extends :js:`Localization` with functionality to operate on HTML, XUL
|
||||
and the DOM directly including DOM Overlays and Mutation Observers.
|
||||
|
||||
|
||||
l10n.js
|
||||
-------
|
||||
|
||||
l10n.js is a small runtime code which fetches the :html:`<link>` elements specified
|
||||
in the document and initializes the main :js:`DOMLocalization` context
|
||||
on :js:`document.l10n`.
|
||||
|
||||
|
||||
L10nRegistry
|
||||
------------
|
||||
|
||||
L10nRegistry is our resource management service. It replaces :js:`ChromeRegistry` and
|
||||
maintains the state of resources packaged into the build and language packs,
|
||||
providing an asynchronous iterator of :js:`MessageContext` objects for a given locale set
|
||||
and resources that the :js:`Localization` class uses.
|
||||
|
||||
|
||||
.. _Fluent: http://projectfluent.org/
|
||||
.. _DTD: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Localization
|
||||
.. _StringBundle: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Tutorial/Property_Files
|
||||
.. _Firefox Preferences: https://bugzilla.mozilla.org/show_bug.cgi?id=1415730
|
||||
.. _Unprivileged Contexts: https://bugzilla.mozilla.org/show_bug.cgi?id=1407418
|
||||
.. _System Add-ons: https://bugzilla.mozilla.org/show_bug.cgi?id=1425104
|
||||
.. _CLDR: http://cldr.unicode.org/
|
||||
.. _ICU: http://site.icu-project.org/
|
||||
.. _Unicode: https://www.unicode.org/
|
||||
.. _Fluent Syntax Guide: http://projectfluent.org/fluent/guide/
|
||||
.. _Pontoon: https://pontoon.mozilla.org/
|
||||
.. _Plural Rules: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
|
@ -0,0 +1,23 @@
|
|||
======
|
||||
Fluent
|
||||
======
|
||||
|
||||
`Fluent`_ is a new localization system, developed by Mozilla, which aims to replace
|
||||
all existing localization models currently used at Mozilla.
|
||||
|
||||
In case of Firefox it directly superseeds DTD and StringBundle systems providing
|
||||
a large number of new features and improvements over them both, for developers
|
||||
and localizers.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
fluent_tutorial
|
||||
|
||||
Other resources:
|
||||
|
||||
* `Fluent Syntax Guide <http://projectfluent.org/fluent/guide/>`_
|
||||
* `Fluent Wiki <https://github.com/projectfluent/fluent/wiki>`_
|
||||
* `Fluent.js Wiki <https://github.com/projectfluent/fluent.js/wiki>`_
|
||||
|
||||
.. _Fluent: http://projectfluent.org/
|
|
@ -17,4 +17,6 @@ MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
|
|||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
SPHINX_TREES['l10n'] = 'docs'
|
||||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
|
|
|
@ -215,10 +215,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
|
||||
[[package]]
|
||||
name = "blurmac"
|
||||
version = "0.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/servo/devices#1069d67cbacb28b77a3d5dd7f211171c05f32c62"
|
||||
dependencies = [
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"objc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
|
@ -693,10 +693,10 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "device"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/servo/devices#c3b012b0ac4fbc47d1ebc9bd3fc308f599be4ee4"
|
||||
source = "git+https://github.com/servo/devices#1069d67cbacb28b77a3d5dd7f211171c05f32c62"
|
||||
dependencies = [
|
||||
"blurdroid 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"blurmac 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"blurmac 0.1.0 (git+https://github.com/servo/devices)",
|
||||
"blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"blurz 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -3660,7 +3660,7 @@ dependencies = [
|
|||
"checksum bitreader 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "80b13e2ab064ff3aa0bdbf1eff533f9822dc37899821f5f98c67f263eab51707"
|
||||
"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
"checksum blurdroid 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d7daba519d29beebfc7d302795af88a16b43f431b9b268586926ac61cc655a68"
|
||||
"checksum blurmac 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72af3718b3f652fb2026bf9d9dd5f92332cd287884283c343f03fff16cbb0172"
|
||||
"checksum blurmac 0.1.0 (git+https://github.com/servo/devices)" = "<none>"
|
||||
"checksum blurmock 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "68dd72da3a3bb40f3d3bdd366c4cf8e2b1d208c366304f382c80cef8126ca8da"
|
||||
"checksum blurz 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e73bda0f4c71c63a047351070097f3f507e6718e86b9ee525173371ef7b94b73"
|
||||
"checksum brotli 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe87b40996b84fdc56e57c165d93079f4b50cb806598118e692ddfaa3d3c57c0"
|
||||
|
|
|
@ -63,6 +63,9 @@ with Files("mozilla/meta/**"):
|
|||
with Files("mozilla/tests/dom/**"):
|
||||
BUG_COMPONENT = ("Core", "DOM")
|
||||
|
||||
with Files("mozilla/tests/editor/**"):
|
||||
BUG_COMPONENT = ("Core", "Editor")
|
||||
|
||||
with Files("mozilla/tests/fetch/**"):
|
||||
BUG_COMPONENT = ("Core", "DOM")
|
||||
|
||||
|
|
|
@ -469,6 +469,12 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"editor/initial_selection_on_focus.html": [
|
||||
[
|
||||
"/_mozilla/editor/initial_selection_on_focus.html",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"fetch/api/redirect/redirect-referrer.https.html": [
|
||||
[
|
||||
"/_mozilla/fetch/api/redirect/redirect-referrer.https.html",
|
||||
|
@ -1022,6 +1028,10 @@
|
|||
"67a981ba2a4d08b684947ed42aba6648dcd262b4",
|
||||
"testharness"
|
||||
],
|
||||
"editor/initial_selection_on_focus.html": [
|
||||
"da3d0ff5305658e18f51a4f19b34927fb2691e60",
|
||||
"testharness"
|
||||
],
|
||||
"fetch/api/redirect/redirect-referrer-mixed-content.js": [
|
||||
"f9d7ec9cf9fa8c847e45664b05482e3f8c191385",
|
||||
"support"
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>initial selection on focus of contenteditable</title>
|
||||
<!-- if you move this file into cross-browser's directly, you should include
|
||||
editing/include/tests.js for using addBrackets() and get rid of it from
|
||||
this file. -->
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<body>
|
||||
<p id="staticText">out of editor</p>
|
||||
<div id="editor" contenteditable style="min-height: 1em;"></div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
var tests = [
|
||||
{ description: "empty editor should set focus to start of it",
|
||||
content: "{}",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node",
|
||||
content: "[]abc",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node",
|
||||
content: "{}<br>",
|
||||
},
|
||||
{ description: "editor should set selection to before the first <br> node",
|
||||
content: "{}<br><br>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the <p> node",
|
||||
content: "<p>[]abc</p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the <p> node",
|
||||
content: "<p>{}<br></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the first <br> node in the <p> node",
|
||||
content: "<p>{}<br><br></p>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the <span> node",
|
||||
content: "<span>[]abc</span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the <span> node",
|
||||
content: "<span>{}<br></span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the first <br> node in the <span> node",
|
||||
content: "<span>{}<br><br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to before the empty <span> node",
|
||||
content: "{}<span></span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <b> node",
|
||||
content: "{}<b></b>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <i> node",
|
||||
content: "{}<i></i>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <u> node",
|
||||
content: "{}<u></u>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <s> node",
|
||||
content: "{}<s></s>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <code> node",
|
||||
content: "{}<code></code>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <a> node",
|
||||
content: "{}<a href=\"foo.html\"></a>",
|
||||
},
|
||||
{ description: "editor should set selection to before the empty <foobar> node",
|
||||
content: "{}<foobar></foobar>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <input> node",
|
||||
content: "{}<input>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <img> node",
|
||||
content: "{}<img alt=\"foo\">",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the second <span> node",
|
||||
content: "<span></span><span>[]abc</span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <span> node",
|
||||
content: "<span></span><span>{}<br></span>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the first <span> node #1",
|
||||
content: "<span>[]abc</span><span>abc</span>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the first <span> node #2",
|
||||
content: "<span>[]abc</span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the first <span> node #1",
|
||||
content: "<span>{}<br></span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the first <span> node #2",
|
||||
content: "<span>{}<br></span><span>abc</span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the second <span> node since the text node in the first <span> node is only whitespaces",
|
||||
content: "<span> </span><span>[]abc</span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <span> node since the text node in the first <span> node is only whitespaces",
|
||||
content: "<span> </span><span>{}<br></span>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the second <span> node even if there is a whitespace only text node before the first <span> node",
|
||||
content: " <span></span><span>[]abc</span>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <span> node even if there is a whitespace only text node before the first <span> node",
|
||||
content: " <span></span><span>{}<br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the second <p> node",
|
||||
content: "<p></p><p>[]abc</p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <p> node",
|
||||
content: "<p></p><p>{}<br></p>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the first <p> node #1",
|
||||
content: "<p>[]abc</p><p>abc</p>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the first <p> node #2",
|
||||
content: "<p>[]abc</p><p><br></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the first <p> node #1",
|
||||
content: "<p>{}<br></p><p><br></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the first <p> node #2",
|
||||
content: "<p>{}<br></p><p>abc</p>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the second <p> node since the text node in the first <p> node is only whitespaces",
|
||||
content: "<p> </p><p>[]abc</p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <p> node since the text node in the first <p> node is only whitespaces",
|
||||
content: "<p> </p><p>{}<br></p>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the second <p> node even if there is a whitespace only text node before the first <p> node",
|
||||
content: " <p></p><p>[]abc</p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the second <p> node even if there is a whitespace only text node before the first <p> node",
|
||||
content: " <p></p><p>{}<br></p>",
|
||||
},
|
||||
|
||||
{ description: "editor should set selection to start of the text node in the <span> node in the second <p> node",
|
||||
content: "<p><span></span></p><p><span>[]abc</span></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the <span> node in the second <p> node",
|
||||
content: "<p><span></span></p><p><span>{}<br></span></p>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the <span> node in the first <p> node #1",
|
||||
content: "<p><span>[]abc</span></p><p><span>abc</span></p>",
|
||||
},
|
||||
{ description: "editor should set selection to start of the text node in the <span> node in the first <p> node #2",
|
||||
content: "<p><span>[]abc</span></p><p><span><br></span></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #1",
|
||||
content: "<p><span>{}<br></span></p><p><span><br></span></p>",
|
||||
},
|
||||
{ description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #2",
|
||||
content: "<p><span>{}<br></span></p><p><span>abc</span></p>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to before the non-editable <span> node",
|
||||
content: "{}<span contenteditable=\"false\"></span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node even if it has a text node",
|
||||
content: "{}<span contenteditable=\"false\">abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node even if it has a <br> node",
|
||||
content: "{}<span contenteditable=\"false\"><br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to before the non-editable empty <span> node followed by a text node",
|
||||
content: "{}<span contenteditable=\"false\"></span><span>abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node having a text node and followed by another text node",
|
||||
content: "{}<span contenteditable=\"false\">abc</span><span>def</span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by a text node",
|
||||
content: "{}<span contenteditable=\"false\"><br></span><span>abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable empty <span> node followed by a <br> node",
|
||||
content: "{}<span contenteditable=\"false\"></span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node having text node and followed by a <br> node",
|
||||
content: "{}<span contenteditable=\"false\">abc</span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <span> node having a <br> node and followed by another <br> node",
|
||||
content: "{}<span contenteditable=\"false\"><br></span><span><br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to before the non-editable empty <p> node followed by a text node",
|
||||
content: "{}<p contenteditable=\"false\"></p><p>abc</p>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <p> node having a text node and followed by another text node",
|
||||
content: "{}<p contenteditable=\"false\">abc</p><p>def</p>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by a text node",
|
||||
content: "{}<p contenteditable=\"false\"><br></p><p>abc</p>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable empty <p> node followed by a <br> node",
|
||||
content: "{}<p contenteditable=\"false\"></p><p><br></p>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <p> node having text node and followed by a <br> node",
|
||||
content: "{}<p contenteditable=\"false\">abc</p><p><br></p>",
|
||||
},
|
||||
{ description: "editor should set focus to before the non-editable <p> node having a <br> node and followed by another <br> node",
|
||||
content: "{}<p contenteditable=\"false\"><br></p><p><br></p>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to start of it if there is non-editable node before first editable text node",
|
||||
content: "{}<span></span><span contenteditable=\"false\"></span><span>abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to start of it if there is non-editable node having a text node before first editable text node",
|
||||
content: "{}<span></span><span contenteditable=\"false\">abc</span><span>def</span>",
|
||||
},
|
||||
{ description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable text node",
|
||||
content: "{}<span></span><span contenteditable=\"false\"><br></span><span>abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to start of it if there is non-editable node before first editable <br> node",
|
||||
content: "{}<span></span><span contenteditable=\"false\"></span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set focus to start of it if there is non-editable node having a text node before first editable <br> node",
|
||||
content: "{}<span></span><span contenteditable=\"false\">abc</span><span><br></span>",
|
||||
},
|
||||
{ description: "editor should set focus to start of it if there is non-editable node having a <br> node before first editable <br> node",
|
||||
content: "{}<span></span><span contenteditable=\"false\"><br></span><span><br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node",
|
||||
content: "<span>[]abc</span><span contenteditable=\"false\"></span>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having another text node",
|
||||
content: "<span>[]abc</span><span contenteditable=\"false\">def</span>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable text node in the first <span> node even if followed by a non-editable node having a <br> node",
|
||||
content: "<span>[]abc</span><span contenteditable=\"false\"><br></span>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node",
|
||||
content: "<span>{}<br></span><span contenteditable=\"false\"></span>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a text node",
|
||||
content: "<span>{}<br></span><span contenteditable=\"false\">abc</span>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <span> node even if followed by a non-editable node having a <br> node",
|
||||
content: "<span>{}<br></span><span contenteditable=\"false\"><br></span>",
|
||||
},
|
||||
|
||||
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node",
|
||||
content: "<p>[]abc</p><p contenteditable=\"false\"></p>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having another text node",
|
||||
content: "<p>[]abc</p><p contenteditable=\"false\">def</p>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable text node in the first <p> node even if followed by a non-editable node having a <br> node",
|
||||
content: "<p>[]abc</p><p contenteditable=\"false\"><br></p>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node",
|
||||
content: "<p>{}<br></p><p contenteditable=\"false\"></p>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a text node",
|
||||
content: "<p>{}<br></p><p contenteditable=\"false\">abc</p>",
|
||||
},
|
||||
{ description: "editor should set focus to the first editable <br> node in the first <p> node even if followed by a non-editable node having a <br> node",
|
||||
content: "<p>{}<br></p><p contenteditable=\"false\"><br></p>",
|
||||
},
|
||||
];
|
||||
|
||||
// This function is copied from editing/include/tests.js
|
||||
function addBrackets(range) {
|
||||
// Handle the collapsed case specially, to avoid confusingly getting the
|
||||
// markers backwards in some cases
|
||||
if (range.startContainer.nodeType == Node.TEXT_NODE ||
|
||||
range.startContainer.nodeType == Node.COMMENT_NODE) {
|
||||
if (range.collapsed) {
|
||||
range.startContainer.insertData(range.startOffset, "[]");
|
||||
} else {
|
||||
range.startContainer.insertData(range.startOffset, "[");
|
||||
}
|
||||
} else {
|
||||
var marker = range.collapsed ? "{}" : "{";
|
||||
if (range.startOffset != range.startContainer.childNodes.length &&
|
||||
range.startContainer.childNodes[range.startOffset].nodeType == Node.TEXT_NODE) {
|
||||
range.startContainer.childNodes[range.startOffset].insertData(0, marker);
|
||||
} else if (range.startOffset != 0 &&
|
||||
range.startContainer.childNodes[range.startOffset - 1].nodeType == Node.TEXT_NODE) {
|
||||
range.startContainer.childNodes[range.startOffset - 1].appendData(marker);
|
||||
} else {
|
||||
// Seems to serialize as I'd want even for tables . . . IE doesn't
|
||||
// allow undefined to be passed as the second argument (it throws
|
||||
// an exception), so we have to explicitly check the number of
|
||||
// children and pass null.
|
||||
range.startContainer.insertBefore(document.createTextNode(marker),
|
||||
range.startContainer.childNodes.length == range.startOffset ?
|
||||
null : range.startContainer.childNodes[range.startOffset]);
|
||||
}
|
||||
}
|
||||
if (range.collapsed) {
|
||||
return;
|
||||
}
|
||||
if (range.endContainer.nodeType == Node.TEXT_NODE ||
|
||||
range.endContainer.nodeType == Node.COMMENT_NODE) {
|
||||
range.endContainer.insertData(range.endOffset, "]");
|
||||
} else {
|
||||
if (range.endOffset != range.endContainer.childNodes.length &&
|
||||
range.endContainer.childNodes[range.endOffset].nodeType == Node.TEXT_NODE) {
|
||||
range.endContainer.childNodes[range.endOffset].insertData(0, "}");
|
||||
} else if (range.endOffset != 0 &&
|
||||
range.endContainer.childNodes[range.endOffset - 1].nodeType == Node.TEXT_NODE) {
|
||||
range.endContainer.childNodes[range.endOffset - 1].appendData("}");
|
||||
} else {
|
||||
range.endContainer.insertBefore(document.createTextNode("}"),
|
||||
range.endContainer.childNodes.length == range.endOffset ?
|
||||
null : range.endContainer.childNodes[range.endOffset]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var editor = document.getElementById("editor");
|
||||
var textInP = document.getElementById("staticText").firstChild;
|
||||
var selection = document.getSelection();
|
||||
for (var i = 0; i < tests.length; i++) {
|
||||
test(function() {
|
||||
// Select outside the editor.
|
||||
editor.blur();
|
||||
selection.collapse(textInP);
|
||||
|
||||
// Initialize the editor content.
|
||||
editor.innerHTML = tests[i].content.replace(/[{}\[\]]/g, "");
|
||||
|
||||
// Make the editor focused.
|
||||
editor.focus();
|
||||
|
||||
assert_equals(selection.rangeCount, 1);
|
||||
if (selection.rangeCount) {
|
||||
addBrackets(selection.getRangeAt(0));
|
||||
assert_equals(editor.innerHTML, tests[i].content);
|
||||
}
|
||||
}, tests[i].description);
|
||||
}
|
||||
|
||||
test(function() {
|
||||
// Check if selection is initialized after temporarily blurred.
|
||||
editor.innerHTML = "<p>abc</p><p>def</p>";
|
||||
editor.focus();
|
||||
// Move selection to the second paragraph.
|
||||
selection.collapse(editor.firstChild.nextSibling.firstChild);
|
||||
// Reset focus.
|
||||
editor.blur();
|
||||
editor.focus();
|
||||
// Then, selection should still be in the second paragraph.
|
||||
assert_equals(selection.rangeCount, 1);
|
||||
if (selection.rangeCount) {
|
||||
addBrackets(selection.getRangeAt(0));
|
||||
assert_equals(editor.innerHTML, "<p>abc</p><p>[]def</p>");
|
||||
}
|
||||
}, "editor shouldn't reset selection when it gets focus again");
|
||||
})();
|
||||
</script>
|
Загрузка…
Ссылка в новой задаче