HWeb-标准的创造性-三-

102 阅读33分钟

HWeb 标准的创造性(三)

原文:Web Standards Creativity

协议:CC BY-NC-SA 4.0

九、使用 JavaScript 创建动态界面

卡梅隆·亚当斯有一个法律学位和一个科学学位。很自然,他选择了 web 开发这一职业。当被追问时,他自称为“网络技术专家”,因为他喜欢参与图形设计、JavaScript、CSS、Perl(是的,Perl)以及那天早上他喜欢的任何东西。他经营自己的公司,为政府部门、非营利组织、大公司和小型创业公司工作过。

除了帮助他的客户名单,卡梅伦还在全国各地的许多研讨会上授课,并在世界各地的会议上发表演讲,如媒体和网络方向。他在 2006 年发布了他的第一本书,JavaScript 选集,这是关于现代 JavaScript 技术最完整的问答资源之一。

卡梅伦住在澳大利亚的墨尔本,在马拉松比赛间隙,他喜欢踢足球,为愤怒的邻居混音。

Creating Dynamic Interfaces Using JavaScript

针对不同需求的不同布局

随着互联网变得越来越普及,用户开始从越来越多的设备访问网页,如笔记本电脑、台式机、PDA、移动电话、冰箱等。。。谁知道拐角处会有什么?每个新设备都有不同的设计限制。手机肯定没有台式电脑的屏幕空间,所以用户可以和应该查看内容的方式将会大不相同。

即使在同一台设备上,不同的用户也有不同的需求。访问者来你的网站做不同的事情,你的界面应该能够适应所有这些。您希望能够为用户提供适合其所选设备和用途的最佳体验。

本章探索了使用 JavaScript 和 CSS 处理这些设计问题的技术。我称这些为动态接口*。我们将着眼于创建两种类型的布局:分辨率相关的布局和用户控制的模块化布局。示例的所有源文件都可以从www.friendsofed.com/下载。*

*# 分辨率相关的布局

很明显,不同的设备有不同的显示能力。这一点在比较移动设备(如 PDA)和它们的桌面兄弟时表现得最为明显。PDA 的分辨率可以低至 240320,而桌面显示器可以高达 20481536 甚至更高。即使在台式机上,分辨率的差异也是惊人的。你的父母可能正在浏览一个(现在是史前的)800600 系统,而你的超级用户姐姐正在浏览一个 1280960 屏幕。两者之间的差异是巨大的,可用面积增加了 150%以上。并且每一种的设计要求同样不同。将宽屏和双屏系统组合在一起,可能性就会爆炸。

有了如此大的差异,说“好吧,创建一个适用于所有分辨率的流畅布局”就不再可行了我们在这里讨论的是页面布局方式的根本区别。分辨率越高,人们看到的信息就越多,因此您的设计应该考虑到这一点。分辨率越低,人们看到的信息就越少,您的设计也应该能够处理这种情况。

Resolution-dependent layouts

问题是,如果你试图让你的设计一刀切,你会遇到不可调和的分歧。在 1280960 中,四列文本可能会为你的内容产生最简洁、最有条理、最可见的结构,但是给一个 800600 显示器上的人四列文本只会产生一团乱麻。相反,在 800600 时工作得相当舒适的单栏设计在 1280960 时看起来会痛苦地拉伸。

传统上,一个答案是简单地限制页面的宽度,将其锁定在一个固定的宽度,在 800 像素(或您选择的基本分辨率)或以上的宽度下产生良好、可读的行长度。然而,这往往不公平地限制了那些利用高质量技术的人。分辨率较高的用户从他们的投资中得不到任何好处;他们只是在内容的两边获得了更多的空间。

最近的统计(www.thecounter.com/stats/2006/August/res.php)表明,屏幕分辨率为 800600 的用户约占市场的 16%,约 55%的用户分辨率为 1024768,约 21%的用户分辨率高于 1024768(任何特定网站的实际统计数据将因受众而异)。通过设计 800600 的分辨率,你在为少数人的情况惩罚你的大多数用户,尽管是相当大的少数人。

那么,有没有一种方法可以让大屏幕用户从辛苦获得的像素中受益,而不损害低分辨率用户的利益呢?是的,通过提供不同尺寸的不同设计。

起初,这可能听起来工作量过大。但是由于基于标准的布局的灵活性,很可能甚至很容易使用完全相同的 HTML 标记并通过改变 CSS 提供不同分辨率的不同设计。

通过比较 UX 杂志(www.uxmag.com)和白皮书(www.whitepages.com.au)网站的不同布局,如图图 9-1 到图 9-4") 所示,你可以看到,页面的设计美学不必为了适应不同的浏览器大小而有很大的改变。大多数情况下,只需对主要内容区域进行重组,就足以创建更流畅的布局,最大限度地利用屏幕空间。针对更宽屏幕优化的布局通常会降低页面高度,从而在折叠上方显示更多内容,使用户无需滚动即可看到页面的更多部分。假设你的标题、菜单、表单和其他网站附件都是用一些漂亮的、健壮的 CSS 编码的,它们应该很容易适应任何一种设计。

Resolution-dependent layouts The UX Magazine website layout optimized for a maximized browser window at 800600

图 9.1。UX 杂志网站布局针对 800600 的最大化浏览器窗口进行了优化

The UX Magazine website layout optimized for a maximized browser window at 1024768

图 9.2。UX 杂志网站布局针对 1024768 的最大化浏览器窗口进行了优化

The White Pages website layout optimized for narrower browser windows (less than 1200px wide)

图 9.3。针对较窄的浏览器窗口(宽度小于 1200 像素)优化的白页网站布局

The White Pages website layout optimized for wider browser windows (greater than 1200px wide)

图 9.4。针对更宽的浏览器窗口(宽度大于 1200 像素)优化的白页网站布局

浏览器大小,而非分辨率

尽管窗口大小通常与屏幕分辨率有关,但两者并不相同。浏览器窗口不一定要最大化,所以不能假设浏览器大小会和屏幕分辨率匹配。根据轶事证据,1024768 和更低分辨率的用户倾向于用最大化的窗口浏览,而分辨率更高的用户更有可能用多个未最大化的窗口进行多任务处理。

我在这里提出的解决方案没有基于屏幕分辨率的假设(即使它被称为依赖于分辨率的布局)。我们的目标是浏览器窗口的布局,所以如果大屏幕上的用户碰巧在小窗口中浏览,他们仍然会得到最佳的小窗口布局。

分辨率相关的布局也不仅仅适用于固定宽度的网站。您仍然可以将流畅的布局作为样式表的一部分。这意味着您将获得两个世界的最佳结果:布局不仅考虑了浏览器大小之间的小不一致(侧边栏、滚动条、最大化/非最大化等等),而且还处理了大的变化,为我们提供了足够的额外空间来保证内容的重组。

多个 CSS 文件

创建分辨率相关布局的第一步是创建不同分辨率所需的不同样式表。

交替样式表和样式切换已经被普遍使用了一段时间。人们普遍认为,当用户打印出一页时,他们需要的格式与他们在屏幕上看到的不同。这就是为什么我们有交替打印样式表。许多网站都有替代的样式表,用于更大的文本、高对比度的布局,或者只是简单的不同外观。所以我们在这里要做的是为不同的分辨率制作一个替代的样式表。这通常需要更改内容以获得更大的页面宽度。

与任何替代样式表一样,首先需要定义基本样式。你的目标是哪个基线人群?目前,800600 通常被认为是最低的普通桌面分辨率,所以我们将使用它作为我们的默认大小。

接下来,您需要决定哪个(些)决议将获得一个替代样式表。什么分辨率会从不同的布局中获得最大的好处?您可以为许多不同的分辨率提供许多不同的布局,但是为了简单起见,我们将只创建一个备选样式表。我们将使用 1024768 作为备用布局的开关。它不像 800600 到 12801024 那样激进,但仍然提供了足够的空间来保证布局的改变。

图 9-5 显示了我们的基本设计,针对 800600 进行了优化。它是流动的,所以它也适用于较小的屏幕。图 9-6 显示了我们针对 1024768 和更高分辨率的设计。

The sample page, optimized for 800600

图 9.5。针对 800600 优化的示例页面

The sample page, optimized for 1024768

图 9.6。针对 1024768 优化的示例页面

这两种布局之间有两个主要变化:

  • 较大样式的内容呈现在页面的四列中,而较小样式的内容保持在一列中。四列布局可以在更高的分辨率下更好地利用空间,并降低页面高度。在较小的屏幕尺寸下,列变得太窄而没有用,因此单列产生更好的内容宽度。

  • 菜单的位置不同。当使用水平菜单时,随着菜单项数量的增加,我们经常会在较小的屏幕上遇到问题。最终,它们会绕成两行。为了解决这个问题,我们将菜单的方向从水平改为垂直,并将其放置在内容旁边。

使用 CSS,通过修改宽度和浮动元素,可以相对容易地实现这两种改变。例如,在 800600 中,三个主要内容面板都使用其默认的浏览器样式:跨越其容器 100%的块元素。为了让它们在 1024768 样式表中并排排列,我们让它们向左浮动并提供一个宽度:

.panel
{
  float: left;
  width: 26.5%;
}

这两种布局都使用了包含在resolution.htm中的完全相同的 HTML 标记,所以您只需要修改样式表。

因为备用样式表是主样式表的补充,所以我们继续对 800600 样式表进行编码,就像我们通常所做的那样,无论如何它都会被应用。为了获得更宽的样式表,我们扩展了基本样式。我们不需要编写一个全新的样式表;我们只是修改和增加已经存在的风格。

正如您将在本例的可下载文件中看到的,基本样式表main.css有 442 行。另一个样式表wide.css要小得多,只有 190 行。

为了在页面上包含两个样式表,我们使用了link元素:

<link rel="stylesheet" type="text/css" 
   href="css/main.css" />
<link rel="alternate stylesheet" 
   type="text/css" href="css/wide.css"
   title="Wide" />

主样式表的rel属性被设置为stylesheet,因此它将自动应用于页面。通过将替换样式表的rel属性设置为alternate stylesheet,我们向浏览器表明,除非指定,否则不应该应用它。为备用样式表包含一个title属性也很重要,这样我们可以在以后识别它。

但是当我们需要它的时候,我们如何打开替换样式表呢?

开启风格

我们有几个选项可以向用户提供这种新的依赖于分辨率的样式表,其中大多数都非常无效:

  • 在 HTML 中包含替代样式表,并希望用户使用的浏览器允许在替代样式表之间切换,并且意识到有替代样式表,然后会费心打开它,如果它适用于他(不会发生)。

  • 使用页面内样式表切换器。在每个页面上创建一个小部件,允许用户指定她希望页面以何种风格显示(就像 Paul Sowden 在他的文章《与列表分离》(on A List Apart,http://alistapart.com/stories/alternate/)中描述的那样)。这有点像原生浏览器选项,除了它更容易被更多的浏览器看到和访问。然而,以我的经验来看,用户不会特意去改变页面的默认外观,即使这对他们有好处。

  • 自动做。将更宽的样式表提供给能够处理它的浏览器。给用户最好的体验,而不用动一根手指。

你可能已经猜到我们要走第三条路,因为这就是分辨率相关布局的意义所在。方法是使用一些 JavaScript 在页面加载时检测窗口大小,如果合适的话应用更宽的样式表。如果 JavaScript 恰好关闭,页面不会爆炸。相反,用户将获得基本的样式表,如果设计得当,它应该仍然完全可用。就把这种替代布局看作是大屏幕用户和 JavaScript 用户的额外收获吧(这是一个相当大的用户群体)。

JavaScript 应该放在 HTML 中的样式表声明之后:

<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="alternate stylesheet" type="text/css" href="css/wide.css" title="Wide" />
<script type="text/javascript" src="scripts/resolution.js"></script>

这很重要,因为 JavaScript 将会立即运行并尝试处理样式表,所以如果它们还没有被包含进来,就会有问题。

resolution.js中要做的第一件事是检查浏览器窗口的大小:

checkBrowserWidth();

function checkBrowserWidth()
{
  var theWidth = getBrowserWidth();

  if (theWidth == 0)
  {
    addLoadListener(checkBrowserWidth);

    return false;
  }

  if (theWidth >= 960)
  {
    setStylesheet("Wide");
  }
  else
  {
    setStylesheet("");
  }

  return true;
};

checkBrowserWidth()首先使用getBrowserWidth()获得浏览器的宽度:

function getBrowserWidth()
{
  if (window.innerWidth)
  {
    return window.innerWidth;
  }
  else if (document.documentElement && document.documentElement.clientWidth != 0)
  {
return document.documentElement.clientWidth;
  }
  else if (document.body)
  {
    return document.body.clientWidth;
  }

  return 0;
};

实际上有几种方法可以确定浏览器窗口的宽度,这取决于你使用的浏览器,而getBrowserWidth()中的每个条件都符合其中一个。火狐、Mozilla、Safari、Opera 使用window.innerWidth。Internet Explorer 6 和 7 使用document.documentElement.clientWidth

奇怪的是,document.documentElement.clientWidth在 Internet Explorer 5 和 5.5 中存在,但它总是被设置为 0,所以我们必须检查这一点,如果是这样的话,就默认为document.body.clientWidth。唯一的问题是body元素必须在 Internet Explorer 5 之前存在。 x 可以计算出浏览器窗口的宽度,所以一旦页面被加载,我们必须重新安排对getBrowserWidth()的调用。这是在checkBrowserWidth()里面的第一个条件下完成的。如果getBrowserWidth()返回 0,说明浏览器很可能是 Internet Explorer 5。 x ,所以我们创建了一个新的 load 事件监听器,并在进行任何样式表切换之前等待页面加载。

addLoadListener()是一个通用的页面加载事件处理程序,它抽象了浏览器事件处理中的一些差异:

function addLoadListener(fn)
{
  if (typeof window.addEventListener != 'undefined')
  {
    window.addEventListener('load', fn, false);
  }
  else if (typeof document.addEventListener != 'undefined')
  {
    document.addEventListener('load', fn, false);
  }
  else if (typeof window.attachEvent != 'undefined')
  {
    window.attachEvent('onload', fn);
  }
  else
  {
    return false;
  }

  return true;
};

这个和另一个抽象的事件处理程序包含在event_listeners.js中,因为您可能会在其他项目中经常用到它们。

然而,大多数浏览器不需要经历繁琐的页面加载。checkBrowserWidth()将立即收到浏览器宽度,并能够检查它。这是通过一个简单的条件来完成的,我们对照预定值检查theWidth。在我们的例子中,我们使用 960 作为阈值,因为对于分辨率为 1024768 的最大化窗口大小来说,这是一个很好的安全值,如果我们减去浏览器 chrome(如滚动条)的空间。如果您希望您的样式表以不同的分辨率更改,只需将该值更改为您需要的值。

如果窗口宽度大于或等于 960 像素,我们通过使用适当的样式表标题调用setStylesheet()来激活替代样式表,在本例中是Wide:

function setStylesheet(styleTitle)
{
  var links = document.getElementsByTagName("link");

  for (var i = 0; i < links.length; i++)
  {
    if (links[i].getAttribute("rel") == "alternate stylesheet")
    {
      links[i].disabled = true;

      if(links[i].getAttribute("title") == styleTitle)
      {
        links[i].disabled = false;
      }
    }
  }

  return true;
};

setStylesheet()获取一个样式表标题并遍历页面中的所有link元素,检查它们的rel属性是否与alternate stylesheet匹配。如果那个link是一个替换样式表,它的disabled属性被设置为true(关闭任何未被选择的样式表),然后检查title属性看它是否是Wide。如果匹配,disabled设置为false(打开样式表)。

该循环完成后,所选的替代样式表将被打开,而所有其他样式表将被关闭。如果浏览器宽度不大于我们的阈值,则使用相同的函数。我们用一个空字符串("")调用setStylesheet(),这将关闭所有可选的样式表,因为它们的标题都不匹配。

一旦所有这些都发生了,正确的样式表将被选择,页面现在应该以适合用户浏览器大小的最佳布局显示。

最后要做的事情是添加一个事件侦听器,以便在有人调整浏览器窗口大小时使用。当某人第一次访问一个页面时,检测浏览器的大小是很好的,但是如果他改变了窗口的大小,我们就要相应地改变布局。为此,我们只需为窗口调整大小事件添加一个事件监听器,再次调用checkBrowserWidth():

attachEventListener(window, "resize", checkBrowserWidth, false);

同样,这里我使用了一个通用的事件处理程序(来自event_listeners.js),它抽象了浏览器事件处理中的一些差异:

function attachEventListener(target, eventType, functionRef, capture)
{
  if (typeof target.addEventListener != "undefined")
  {
    target.addEventListener(eventType, functionRef, capture);
}
  else if (typeof target.attachEvent != "undefined")
  {
    target.attachEvent("on" + eventType, functionRef);
  }
  else
  {
    return false;
  }

  return true;
};

针对 Internet Explorer 5.x 的优化

如前所述,Internet Explorer 5。x 浏览器在获得浏览器宽度之前必须等待页面加载。交换样式表时,这可能会导致一点视觉闪烁。为了在某种程度上改善这一点,我们可以设置一个 cookie,在用户第一次访问网站时存储浏览器宽度,然后在重复加载时使用这个值。这样,用户只会在第一次访问时得到一个闪烁。为此,我们需要修改checkBrowserWidth() :

function checkBrowserWidth()
{
  var theWidth = getBrowserWidth();

  if (theWidth == 0)
  {
    var resolutionCookie = document.cookie.match(/(^|;) res_layout[^;]*(;|$)/);

    if (resolutionCookie != null)
    {
      setStylesheet(unescape(resolutionCookie[0].split("=")[1]));
    }

    addLoadListener(checkBrowserWidth);

    return false;
  }

  if (theWidth >= 960)
  {
    setStylesheet("Wide");
    document.cookie = "res_layout=" + escape("Wide");
  }
  else
  {
    setStylesheet("");
    document.cookie = "res_layout=";
  }

  return true;
};

现在,一旦浏览器宽度已经确定,样式表已经切换,我们还设置了一个名为res_layout的 cookie 来存储应用的样式表的标题。在重复访问时,如果浏览器宽度返回为 0(即,它是 Internet Explorer 5。 x ,我们检查res_layout的 cookie 值是否存在,如果存在,立即相应地设置样式表。这消除了闪烁。我们仍然继续添加调用getBrowserWidth()的负载监听器,所以如果用户在设置 cookie 后碰巧改变了浏览器的大小,我们可以将布局调整到新的宽度。

我们已经完成了分辨率相关的布局。让我们转向另一种方法。

模块化布局

根据用户的浏览器大小提供静态的布局选择是一回事,但是让用户个人控制他们在你的页面上看到什么以及他们在哪里看到它是另一回事。

模块化布局为用户提供了一个视觉和行为框架,允许他们从根本上改变界面在网页上的实时显示方式。一旦设计者将内容划分为独立的内容模块,这些模块就可以被展开和折叠,它们的顺序可以被重新组织,它们在页面上的位置可以被用户改变。

注意

允许用户定制页面不是你应该随意提供的。如果一个人在一生中只花十分钟的时间,他不太可能会花时间去研究一个界面并根据自己的喜好修改它。然而,当你提供的服务旨在让用户经常回到你的网站,当他们可以通过定制界面获得一些浏览或工作流程效率时,拥有一个可定制的布局可以是一个非常宝贵的工具。最有可能选择可定制布局的是 web 应用程序,在这种应用程序中,用户可能只想执行应用程序提供的任务的一部分,或者他们经常使用应用程序中的特定功能。

图 9-7 显示了一个带有模块布局的样本页面的默认视图,我们将构建它来演示动态布局技术。中心内容保持不变。我们将重点关注两侧的辅助模块。

The default view of the modular layout example

图 9.7。模块化布局示例的默认视图

图 9-8 显示了一个修改后的界面视图,在用户通过折叠、重组和移动的组合按照自己的喜好重新排列模块后。

A user-defined view of the modular layout example

图 9.8。模块化布局示例的用户自定义视图

我们使用的例子反映了各种门户页面。用户可能会定期访问该网站以查看不同类型信息的集合,某些信息的重要性因用户而异。

所有的界面定制都必须用 JavaScript 来完成。这意味着不支持 JavaScript 的用户代理将无法访问定制功能。这些用户将收到页面的默认视图,并能够访问所有信息,就像它是一个普通的静态网页一样。

加价

页面内容的高层结构相当简单。左侧的数据模块包含在各自的部分中,接着是中心内容,然后是右侧的更多数据模块:

<div id="modules1">
. . .
</div>
<div id="news">
. . .
</div>
<div id="modules2">
. . .
</div>

这里要注意的最重要的事情是,如果用户将内容从一个模块区域移动到另一个模块区域,它必须被放置到适当的容器中。

注意

网页的 HTML 不必严格遵循 modular.htm 的模板。然而,示例中使用的 JavaScript 和 CSS 是为该标记定制的,需要根据特定页面的布局方式进行调整。

在每个模块区域内,我们使用一个带有类modulediv将每个模块分成它自己的部分:

<div id="modules1">
  <div class="module">
    <h2>
      Search
    </h2>
    <div class="moduleContent">
. . .
    </div> <!-- END .moduleContent -->
  </div> <!-- END .module -->

我们的大部分 JavaScript 将关注这个h2元素,因为它提供了执行展开/折叠和移动动作的句柄。我们不关心包含在moduleContent div中的 HTML,因为它是特定于每个模块的。我们只想抽象出我们的框架来处理每个模块块。

展开和折叠模块

扩展和折叠内容的能力并不是一个新概念,实际上非常简单。也许我们能做的最重要的事情就是让它不引人注目,容易接近。

另一方面,没有打开 JavaScript 的用户不应该被提供他们无法访问的功能。这意味着允许展开/折叠的元素应该通过 JavaScript 本身来包含。

JavaScript

当页面上包含expand_collapse.js时,它会设置一个页面加载监听器,然后遍历每个模块并插入适当的 HTML 元素:

addLoadListener(initExpandCollapse);

function initExpandCollapse()
{
  var modules = [document.getElementById("modules1"), document.getElementById("modules2")];

  for (var i in modules)
  {
    var h2s = modules[i].getElementsByTagName("h2");

    for (var i = 0; i < h2s.length; i++)
    {
      var newA = document.createElement("a");
      newA.setAttribute("href", "#");
      newA.setAttribute("title", "Expand/Collapse");
      attachEventListener(newA, "mousedown", mousedownExpandCollapse,false);
      newA.onclick = clickExpandCollapse;

        var newImg = document.createElement("img");
        newImg.setAttribute("src", "img/min_max.gif");
        newImg.setAttribute("alt", "Expand/Collapse");
        newA.appendChild(newImg);

      h2s[i].appendChild(newA);
    }
  }

  return true;
};

页面加载监听器是使用与分辨率相关的布局示例相同的event_listeners.js文件中的addLoadListener()函数添加的,并在页面准备就绪时调用initExpandCollapse()

initExpandCollapse()首先创建感兴趣的模块元素的数组,然后遍历该数组,找到所有的h2元素。对于每个h2,我们创建一个新的anchor元素,它包含一个img元素(我们的小展开/折叠图标),然后我们对那个anchor应用一些行为。通过使用一个anchor元素而不是spandiv,我们确保了展开/折叠功能是键盘可访问的。anchor元素可通过键盘聚焦,当用户按下回车键激活它们时,会收到一个点击事件。

我们在anchor标签上捕捉两个事件:鼠标按下和点击。mousedown 侦听器需要抵消我们将用于拖放内容模块的 mousedown。本质上,我们想要监听anchor上的点击,但是每当有人点击鼠标按钮时,它之前也会有鼠标按下。因为anchorh2内部,这将触发h2的 mousedown 事件,所以我们需要在anchor的 mousedown 事件触发时取消h2的 mousedown 事件。这是通过mousedownExpandCollapse() : 完成的

function mousedownExpandCollapse(event)
{
  if (typeof event == "undefined")
  {
    event = window.event;
  }
if (typeof event.stopPropagation != "undefined")
  {
    event.stopPropagation();
  }
  else
  {
    event.cancelBubble = true;
  }

  return true;
};

这个简短的函数主要是为了避免浏览器在事件处理上的差异。第一个条件检查一个event对象是否被传递给了函数本身。这通常由事件侦听器自动完成,但在 Internet Explorer 5 中不会发生。 x 。如果事件对象不存在,我们默认为window.event,也就是 Internet Explorer 5。 x 用途。

接下来,我们检查event对象方法stopPropagation()是否存在。这是阻止事件向上冒泡的 W3C 标准方法(也就是说,阻止anchor上的 mousedown 事件也在h2上注册)。如果stopPropagation()存在,我们称之为。然而,Internet Explorer 使用名为cancelBubbleevent对象的专有属性来停止事件冒泡。如果stopPropagation()不存在,我们就把event.cancelBubble设为true,这也是一样的效果。

现在我们需要处理点击事件。您可能已经注意到,为了向anchor添加 mousedown 事件监听器,我们使用了来自依赖于分辨率的布局attachEventListener()的抽象函数,但是对于 click 事件,我们实际上使用了一个旧的事件处理程序,即onclick属性。因为 Safari 不允许我们停止使用 W3C 事件侦听器添加的默认点击事件操作,所以我们需要恢复到点属性。实际上,这不会造成太大的问题。我们将事件处理程序添加到由我们自己的脚本创建的元素中,因此它不应该附加任何其他可能冲突的事件信息。

点击事件处理程序指向的函数是clickExpandCollapse():

function clickExpandCollapse()
{
  if (!hasClass(this.parentNode.parentNode, "collapsed"))
  {
    addClass(this.parentNode.parentNode, "collapsed");
  }
  else
  {
    removeClass(this.parentNode.parentNode, "collapsed");
  }

  return false;
};

这在包围着h2div上做了一个简单的类切换。如果那个div没有collapsed的类,我们添加这个类。如果它有一个collapsed类,我们删除这个类。这使得完全通过 CSS 来设计折叠和展开模块的不同视图变得容易。

在处理clickExpandCollapse()中的元素类时,我们总是使用这些自定义函数:hasClass()addClass()removeClass()。这使得在同一个元素上使用多个类名变得更加容易。我已经将这些函数包含在它们自己的库文件class_names.js中,所以它们可以很容易地包含在其他项目中:

function hasClass(target, classValue)
{
  var pattern = new RegExp("(^| )" + classValue + "( |$)");
if (target.className.match(pattern))
  {
    return true;
  }

  return false;
};

function addClass(target, classValue)
{
  if (!hasClass(target, classValue))
  {
    if (target.className == "")
    {
      target.className = classValue;
    }
    else
    {
      target.className += " " + classValue;
    }
  }

  return true;
};

function removeClass(target, classValue)
{
  var removedClass = target.className;
  var pattern = new RegExp("(^| )" + classValue + "( |$)");

  removedClass = removedClass.replace(pattern, "$1");
  removedClass = removedClass.replace(/ $/, "");

  target.className = removedClass;

  return true;
};

对一个元素的类进行直接的字符串比较是不正确的,因为它实际上可以包含多个类,每个类之间用空格隔开。这与您不应该对类进行直接赋值的原因是一样的,因为您可能会覆盖现有的类。hasClass()检查您正在搜索的值是否是多个类中的一个。addClass()确保您不会覆盖任何现有的类或多次添加同一个类。removeClass()只删除指定的类,而保持所有其他类不变。

现在 click 事件已经就绪,每次用户单击内容模块的展开/折叠链接时,它都会根据需要添加或删除collapsed类。这就完成了我们的展开/折叠行为!

CSS 样式

既然我们已经向页面添加了一些新元素和类,那么是时候对展开/折叠小部件进行样式化了。扩展/折叠anchor标签嵌套在h2中,所以我们可以通过 CSS(来自main.css)使用它:

.module h2
{
  position: relative;
}

.module h2 a
{
  position: absolute;
  top: 50%;
  right: 10px;
  width: 9px;
  height: 9px;
  overflow: hidden;
  margin: 4px 0 0 0;
  cursor: pointer;
}

.module h2 a img
{
  display: block;
}

为了让链接与h2的右边对齐,我选择了绝对定位,并使用right定位属性将它放置在距离边缘 10px 的位置(你也可以轻松地将元素向右浮动)。

h2本身是相对定位的,这个对主播有重要作用。通常,当你绝对定位一个元素时,它将相对于整个页面定位;例如,如果您将它的顶部设置为 0,它将移动到页面的顶部。然而,当你将一个绝对定位的元素(比如我们的anchor)嵌套在一个相对定位的元素(比如我们的h2)中时,anchor将会相对于h2进行定位。所以当我们设置anchorright位置时,它将从h2的右边开始定位。

链接的尺寸取决于它所包含的图像的大小。小加号和减号图标的大小是 9px 乘 9px,所以这应该是链接的大小。实际上,包含图标的图像的大小是 9px 乘 18px。这是因为两个图标在同一个图像文件中,如图图 9-9 所示。

通过将两个图标放在同一个图像文件中,我们可以通过 CSS 影响展开/折叠链接的显示。通过将链接本身限制为仅一个图标的大小,然后将hidden值应用到overflow属性,我们有效地裁剪了图像的其余部分,使其看起来像只有一个图标。然后,在main.css里,我们可以在它的“盒子”里移动图像,将图标从减号切换到加号:

The two icons for expanding and collapsing combined in one image

图 9.9。用于展开和折叠的两个图标组合在一幅图像中

.collapsed h2 a img
{
  position: relative;
  top: 9px;
}

默认情况下,将显示减号图标,表示您可以折叠该模块。但是一旦模块被折叠,我们使用collapsed类来指定图标图像现在应该相对于它的包含元素(链接)向上移动 9px,从而显示加号图标。

显示和隐藏模块的实际内容更加容易:

.collapsed .moduleContent
{
  display: none;
}

现在展开和折叠的样式与行为相匹配,如图 9-10 和 ?? 9-11 所示,我们都完成了!

The calendar module in its default, expanded view

图 9.10。默认扩展视图中的日历模块

The calendar module after it has been collapsed

图 9.11。折叠后的日历模块

重组模块

展开/折叠是容易的部分。真正让用户控制的是能够指定模块本身的布局。我们将允许用户抓住一个模块的标题栏,拖动它在它的容器位置上下移动,或者把它换到另一个模块区域。因为重组行为完全独立于展开/折叠行为,所以我们可以将它包含在一个不同的文件modular.js中,以便于维护。

拖放事件监听器

设置新行为的第一步是为拖放操作创建事件侦听器:

addLoadListener(initModular);

function initModular()
{
  var modules = [document.getElementById("modules1"), document.getElementById("modules2")];

  for (var i = 0; i < modules.length; i++)
  {
    var h2s = modules[i].getElementsByTagName("h2");

    for (var j = 0; j < h2s.length; j++)
    {
      addClass(h2s[j].parentNode, "moduleDraggable");
      attachEventListener(h2s[j], "mousedown", mousedownH2, false);
    }
  }

  return true;
};

我们使用addLoadListener()添加了另一个页面加载监听器,但是这一次它调用的是initModular(),它很像initExpandCollapse(),遍历模块区域,找到每个h2元素。对于每个h2,我们向其父类div添加一个新类moduleDraggable,并在h2上创建一个鼠标按下事件监听器。我们将这个类放在h2的父类div上,这样一旦我们知道 JavaScript 已启用,我们就可以添加一些样式调整。我们想给用户一个提示,模块是可移动的(否则,他们可能不会意识到这个事实),所以我们在main.css中使用这个 CSS 规则:

.moduleDraggable h2
{
  cursor: move;
}

当用户将鼠标悬停在一个可拖动的h2元素上时,光标变为移动光标,如图 9-12 中的所示。

通过收集信息和设置变量,mousedown 监听器在h2上调用的函数为模块的拖放行为做了大量工作:

The mouse cursor changes appearance when it is hovered over a module's title bar.

图 9.12。当鼠标光标悬停在模块的标题栏上时,它的外观会发生变化。

function mousedownH2(event)
{
  if (typeof event == "undefined")
  {
event = window.event;
  }

  if (typeof event.target != "undefined")
  {
    dragTarget = event.target.parentNode;
  }
  else
  {
    dragTarget = event.srcElement.parentNode;
  }

  dragOrigin = [event.clientX, event.clientY];
  dragHotspots = [];

  var modules = [document.getElementById("modules1"), document.getElementById("modules2")];

  for (var i = 0; i < modules.length; i++)
  {
    var divs = modules[i].getElementsByTagName("div");

    for (var j = 0; j < divs.length; j++)
    {
      if (divs[j] != null && hasClass(divs[j], "module"))
      {
        var modulePosition = getPosition(divs[j]);

        dragHotspots[dragHotspots.length] =
        {
          element: divs[j],
          offsetX: modulePosition[0],
          offsetY: modulePosition[1]
        }
      }
    }

    var modulePosition = getPosition(modules[i]);

    dragHotspots[dragHotspots.length] =
    {
      element: modules[i],
      offsetX: modulePosition[0],
      offsetY: modulePosition[1] + modules[i].offsetHeight
    }
  }

  var position = getPosition(dragTarget);

var ghost = document.createElement("div");
  ghost.setAttribute("id", "ghost");
  document.getElementsByTagName("body")[0].appendChild(ghost);
ghost.appendChild(dragTarget.cloneNode(true));
  ghost.style.left = position[0] + "px";
  ghost.style.top = position[1] + "px";

  attachEventListener(document, "mousemove", mousemoveDocument, false);
  attachEventListener(document, "mouseup", mouseupDocument, false);

  event.returnValue = false;

  if (typeof event.preventDefault != "undefined")
  {
    event.preventDefault();
  }

  return true;
};

在它完成正常的事件对象抽象之后,mousedownH2()接着开始寻找哪个元素被点击了。这在理论上可以通过引用this对象来实现,但是不幸的是,当使用 W3C 事件模型添加事件时,Internet Explorer 不能正确地分配这个对象。相反,我们必须依赖于event对象的目标元素属性,如果在我们的事件目标中有嵌套的元素,这可能是不准确的,但是在这里没有问题,因为h2是这个分支中最深的元素。自然,Internet Explorer 不会像其他浏览器那样将这个元素称为target;在 ie 浏览器里是srcElement。所以我们必须检查event.target是否存在(标准属性),如果不存在,就使用event.srcElement(Internet Explorer 属性)。

一旦这些差异得到解决,我们就在一个名为dragTarget的全局变量中创建一个对目标元素的父节点(围绕着h2div)的引用,这样我们以后就可以在其他函数中使用它。

识别出目标后,我们创建一些全局变量,帮助跟踪用户拖动模块时发生的情况。dragOrigin存储原始鼠标按下事件的坐标。每当事件被触发时,event.clientXevent.clientY记录两个坐标,这两个坐标定义了页面上事件发生的位置;在这种情况下,用户按下鼠标按钮。存储这个原始位置很重要,因为当用户移动鼠标时,我们想知道她从最初点击的位置移动了多远。

一页地图

是一个数组,存储了你可以拖动一个模块的所有可能的位置。你可以把它想象成一种地图。当用户拖动一个模块时,他们不会改变它在页面上的任意位置;相反,它们实际上改变了它相对于其他模块的位置。因此,我们没有用绝对坐标来指定一个模块的目的地,而是试图用它在其他模块中的位置来描述它的目的地:“将搜索模块移动到日历模块之下,星座模块之上。”为了做到这一点,我们需要创建一个页面地图,指示所有其他模块的位置。这样,当被拖动的模块在页面中移动时,我们知道被拖动的模块应该在每个静态模块的上面还是下面。不过,所有计算都是由 mousemove 处理程序完成的。目前,我们只关心创建地图。

我们可以在 mousemove 处理程序中创建地图,但是每次鼠标移动时,都必须重新进行计算。只需按下鼠标按钮一次就可以创建地图,这样效率更高。

为了创建映射,我们查看每个模块区域的内部,并遍历所有具有类modulediv。对于那些div,我们在dragHotspots数组中创建一个新对象,它保存对模块的引用(element)、模块左上角的水平页面偏移量(offsetX)和模块左上角的垂直页面偏移量(offsetY)。最后两个属性从getPosition()函数中获取值,该函数获取一个元素并返回其位置:

function getPosition(theElement)
{
  var positionX = 0;
  var positionY = 0;

  while (theElement != null)
  {
    positionX += theElement.offsetLeft;
    positionY += theElement.offsetTop;
    theElement = theElement.offsetParent;
  }

  return [positionX, positionY];
};

获取元素在页面上的位置过于复杂,但对此无能为力。元素只能辨别其相对于“偏移父元素”的位置(偏移父元素根据其定位、浮动等方式而变化)。为了获得它在页面上的绝对位置,我们必须递归地沿着偏移父树向上(使用element.offsetParent),获得每个偏移父树的位置,并将它们加在一起返回一个总数。

一旦一个模块区域中的每个模块都被添加到dragHotspots中,有一种特殊的情况是我们将模块区域本身添加到dragHotspots中。这是为了让我们能够识别被拖动的模块何时位于模块区域中所有其他元素的下方;在这种情况下,我们把它移到最后。

鬼魂

创建地图后,我们需要创建可视元素,向用户指示他们正在拖动什么东西。它实际上是用户拖动的模块的副本,我们通过改变它的透明度使它看起来像幽灵一样,所以我称它为幽灵。

我们使用getPosition()获得被拖动模块的当前位置,然后创建新的ghost元素。这是一个绝对定位的空div,包裹着被拖动模块的直接副本。空的div代替了模块区域div的约束,给了模块一个要填充的宽度;否则,它将扩展到 100%的宽度。因为它被绝对定位并插入到 body 元素的末尾,所以它可以自由地移动到用户鼠标光标移动的任何地方。

使用cloneNode()方法创建被拖动模块的副本。cloneNode()接受一个参数,该参数指定是希望节点的内容也被复制,还是只复制节点本身。通过给dragTarget.cloneNode(true)打电话,我们说我们想要一份dragTarget 的内容。我们将该副本添加到空壳中,然后将它放在原始模块上,这样看起来就像用户将幽灵拖出了它的身体。通过应用一点 CSS 不透明度,我们可以给幽灵一个幽灵般的外观:

#ghost .module
{
  opacity: 0.65;
  filter: alpha(opacity=50);
}

你可以在图 9-13 中看到结果。

为了跟踪按钮被按下时用户移动鼠标的位置,我们需要向整个文档添加一个 mousemove 侦听器。我们添加了一个 mouseup 侦听器来告诉我们用户何时释放了鼠标按钮。

The ghost copy of a module next to its original

图 9.13。原模块旁边的模块的幻影副本

现在我们已经为用户拖动模块做好了充分的准备。mousedownH2()做的最后一件事是停止鼠标按下的默认动作。这可以防止浏览器在用户按下鼠标按钮时正常工作。在拖放的情况下,当拖动重影时,它会阻止页面上的文本被选中。

随着用户四处移动鼠标,文档 mousemove 事件监听器将不断调用mousemoveDocument():

function mousemoveDocument(event)
{
  if (typeof event == "undefined")
  {
    event = window.event;
  }

  var ghost = document.getElementById("ghost");

  if (ghost != null)
  {
    ghost.style.marginLeft = event.clientX - dragOrigin[0] + "px";
    ghost.style.marginTop = event.clientY - dragOrigin[1] + "px";
  }

  var closest = null;
  var closestY = null;

  for (var i in dragHotspots)
  {
    var ghostX = parseInt(ghost.style.left, 10) + parseInt(ghost.style.marginLeft, 10);
    var ghostY = parseInt(ghost.style.top, 10) + parseInt(ghost.style.marginTop, 10);

    if (ghostX >= dragHotspots[i].offsetX -
    dragHotspots[i].element.offsetWidth && ghostX <=
    dragHotspots[i].offsetX + dragHotspots[i].element.offsetWidth)
    {
      var distanceY = Math.abs(ghostY - dragHotspots[i].offsetY);

      if (closestY == null || closestY > distanceY)
      {
        closest = dragHotspots[i];
        closestY = distanceY;
      }
    }
  }

  if (closest != null)
  {
    var ghostMarker = document.getElementById("ghostMarker");

    if (ghostMarker == null)
    {
      ghostMarker = document.createElement("div");
      ghostMarker.id = "ghostMarker";
document.getElementsByTagName("body")[0].appendChild(ghostMarker);
    }

    ghostMarker.marked = closest.element;

    ghostMarker.style.left = closest.offsetX + "px";
    ghostMarker.style.top = closest.offsetY + "px";
  }
  else
  {
    var ghostMarker = document.getElementById("ghostMarker");

    if (ghostMarker != null)
    {
      ghostMarker.parentNode.removeChild(ghostMarker);
    }

  }

  event.returnValue = false;

  if (typeof event.preventDefault != "undefined")
  {
    event.preventDefault();
  }

  return true;
};

这个函数的首要任务是改变重影的位置,使它看起来像是在跟随用户的鼠标光标。我们通过计算用户从最初的鼠标按下点移动了多远来做到这一点,然后在 ghost div上设置边距来反映这一差异。使用event.clientXevent.clientY再次获得鼠标光标的当前坐标,全局dragOrigin变量用于原始鼠标按下坐标。

mousedownDocument()然后需要使用我们的其他模块的映射来找出被拖动模块的新插入点。我们知道 ghost div的位置,并且我们知道每个静态模块左上角的位置,所以很容易计算出 ghost 离哪个静态模块最近。我们希望在静态模块之前插入被拖动的模块。

对于dragHotspots中的每一个条目,我们都快速测试一下幽灵的水平位置。如果 ghost 的任何部分都不在静态模块所在的模块区域的宽度内,我们就不希望将它视为被拖动模块的可行目的地。因此我们检查重影的边缘是否在静态模块左边缘的右侧,以及重影的边缘是否在静态模块右边缘的左侧。这意味着重影的某些部分与模块区域的宽度重叠。

然后,我们可以关心幽灵和静态模块之间的垂直距离。为此,我们测量重影顶部和静态模块顶部之间的差异,然后将其与已经找到的最小值进行比较。如果到当前静态模块的距离小于当前最小值,则该值成为新的最小值。在检查完所有的dragHotspots条目后,我们知道是否有合适的位置可以插入被拖动的模块,如果有,在哪里。

如果有合适的位置,我们需要向用户显示,这样他们就知道如果释放鼠标按钮,拖动的模块将被插入到哪里。为此,我们创建了一个名为ghostMarker的新元素,它只是一个位于候选插入点顶部的方形小块。ghost 标记是body元素的绝对定位子元素,所以为了定位它,我们使用最近的静态模块的坐标。正如你在图 9-14 中看到的,通过一点 CSS 样式,ghost 标记很好地指出了被拖动的模块将被插入的位置。

The ghost marker showing where the dragged module will be inserted

图 9.14。显示拖动模块插入位置的重影标记

为了便于以后插入被拖动的模块,我们还创建了一个新的属性ghostMarker,它记录了正在标记哪个静态模块:ghostMarker.marked

如果没有一个合适的位置来插入被拖动的模块,那么当我们实际移除ghostMarker时,用户没有将 ghost 拖动到足够靠近某个静态模块的位置。因此,如果用户将重影从一个合适的位置移动到一个不合适的位置,它仍然不会表明他可以重新定位被拖动的模块。

mousemoveDocument()中的最后一个操作再次停止了浏览器通常为鼠标移动执行的默认动作,防止用户拖动时发生不必要的动作。

重新定位的模块

当用户决定将拖动的模块放在哪里,或者不想移动它时,他将释放鼠标按钮。这是由mouseupDocument()捕获的:

function mouseupDocument()
{
  detachEventListener(document, "mousemove", mousemoveDocument, false);

  var ghost = document.getElementById("ghost");

  if (ghost != null)
  {
    ghost.parentNode.removeChild(ghost);
  }

  var ghostMarker = document.getElementById("ghostMarker");

  if (ghostMarker != null)
  {
    if (!hasClass(ghostMarker.marked, "module"))
    {
ghostMarker.marked.appendChild(dragTarget);
    }
    else
    {
      ghostMarker.marked.parentNode.insertBefore(dragTarget, ghostMarker.marked);
    }

    ghostMarker.parentNode.removeChild(ghostMarker);
  }

  return true;
};

大部分繁重的工作已经完成,所以这个函数只是把事情整理一下。首先,它从文档中删除了 mousemove 事件侦听器,因为我们不再需要知道用户何时移动鼠标。之后,我们移除ghost元素,因为我们想要停止显示副本。

然后我们决定将拖动的模块放在哪里。如果ghostMarker仍然存在,意味着用户在有一个有效的地方移动被拖动的模块时释放了鼠标按钮。使用ghostMarker.marked属性,我们可以看到哪个元素之前插入了被拖动的模块。我们需要检查被标记的模块是否真的是一个模块,或者它是否是我们放在模块区域末尾的额外占位符。如果是一个实际的模块(它有一个module类),我们可以使用insertBefore()方法将被拖动的模块移动到被标记的模块之前。如果是一个模块区域,我们使用appendChild()将被拖动的模块放在区域的末尾。无论哪种方式,我们都删除了重影标记,被拖动的模块现在就在它的新位置结束了,就在用户想要的地方!如果ghostMarker不再存在,我们知道被拖动的模块无处可去,所以我们可以安静地完成。

图 9-15 显示了用户将一个模块从一个模块区拖动到另一个模块区的全过程。

Dragging a module to another module area

图 9.15。将一个模块拖到另一个模块区

Dragging a module to another module area

跟踪变化

本例中介绍的技术创建了一个用户可以随心所欲定制的页面。但是,如果他们第二天返回到该页面,而该页面没有保留他们的任何更改,那么这将是徒劳的。

虽然可以从服务器下载页面,并通过 JavaScript 重新排列模块,但这是一个相当笨拙的过程,可能会导致用户在页面切换到他们的定制布局之前看到默认布局。

最好的解决方案是在服务器上组装自定义布局,并按照用户的安排提供给他们。实现这一点最简单的方法是在用户的浏览器中存储一个 cookie,告诉服务器用户布局的顺序,服务器可以用它来组装适当的页面。

我们所要做的就是跟踪哪些模块在哪个模块区域以及它们的顺序。为此,我们需要确保每个模块在我们的标记中都有一个惟一的 ID。一旦完成,我们可以修改mouseupDocument()函数来编写一些包含偏好数据的 cookies。这个更新的功能包含在modular_cookie.js(你可以包含它而不是modular.js):

function mouseupDocument()
{
  detachEventListener(document, "mousemove", mousemoveDocument, false);

  var ghost = document.getElementById("ghost");

  if (ghost != null)
  {
    ghost.parentNode.removeChild(ghost);
  }

  var ghostMarker = document.getElementById("ghostMarker");

  if (ghostMarker != null)
  {
    if (!hasClass(ghostMarker.marked, "module"))
    {
      ghostMarker.marked.appendChild(dragTarget);
    }
    else
    {
      ghostMarker.marked.parentNode.insertBefore(dragTarget, ghostMarker.marked);
    }

    ghostMarker.parentNode.removeChild(ghostMarker);

    var modules1 = document.getElementById("modules1");
    var modules1Modules = [];
    var divs = modules1.getElementsByTagName("div");

    for (var i = 0; i < divs.length; i++)
    {
if (hasClass(divs[i], "module"))
      {
        modules1Modules[modules1Modules.length] = divs[i].getAttribute("id");
      }
    }

    document.cookie = "modules1=" + modules1Modules.join(",");

    var modules2 = document.getElementById("modules2");
    var modules2Modules = [];
    var divs = modules2.getElementsByTagName("div");

for (var i = 0; i < divs.length; i++)
    {
      if (hasClass(divs[i], "module"))
      {
        modules2Modules[modules2Modules.length] = divs[i].getAttribute("id");
      }
    }

    document.cookie = "modules2=" + modules2Modules.join(",");
  }

  return true;
};

现在,当一个模块被重新定位时,会写入两个 cookies。对于每个模块区域,找到具有类modulediv,并将它们的 id 添加到数组中。该数组将反映模块的顺序,因为getElementsByTagName()按源代码顺序返回元素。一旦数组完成,它就被写入一个 cookie 中,根据具体情况,可以是modules1modules2,每个 id 用逗号分隔。

当这些 cookies 被传递到服务器时,您应该能够使用它们来检查模块的顺序,并以适当的顺序将它们写入 web 页面。

结论

用户需求的多样性——无论是技术性的还是基于任务的——正在挑战传统的静态网页形式。创建动态接口是满足这些需求的方法之一。我在这里描述的两种技术展示了基于标准的 web 页面的灵活性如何改变我们为 Web 设计的方式。

Conclusion*

十、无障碍滑动导航

德里克

费瑟斯通

boxofchocolates.ca

furtherahead.com

德里克·费瑟斯通(Derek Featherstone)迷人、令人惊讶、鼓舞人心,他有一种天赋,能够以全新的视角看待 web 开发的几乎每一个方面,并以一种重新点燃我们的热情的方式进行教学,让 Web 为每个人变得更好。费瑟斯通是国际知名的可访问性和 web 开发权威,也是受人尊敬的技术培训师和作家。

作为关于 HTML、CSS、DOM 脚本和 Web 2.0 应用程序的深入课程的创建者,Derek 从不忘记支持 Web 标准和通用可访问性的事业。自 1999 年以来,他通过自己的公司 the Further Ahead(www.furtherahead.com)成为政府机构、教育机构和私营企业的抢手顾问。他丰富的经验和洞察力使他能够为观众提供直接适用的,非常简单的方法来应对网站设计中的日常挑战。

Derek 是可访问性任务组的负责人,也是 Web 标准项目的 DOM 脚本任务组的成员。他还在自己广受欢迎的博客和个人网站www.boxofchocolates.ca上评论各种话题。

Accessible Sliding Navigation

杀手锏

承认吧。你想要它。你知道,你网站上的杀手锏让博客圈说“aaaaaaahhhhhh。”为你赢得重启奖的那个。让你的网站脱颖而出的那个。好吧,算了。通过为访问者提供更容易的信息和链接,让他们的生活变得更容易的那个呢?

我们现在谈论的是老式动态 HTML 菜单系统的替代品,这种系统只需简单的鼠标悬停和点击,就可以访问所有的分类和子分类链接。这个替代品是滑动导航系统,它显示了很好分类的链接。

肖恩·因曼的博客(www.shauninman.com)在他 2005 年的重新设计中展示了这一技术,完成了一个滑动导航和搜索标签,隐藏和暴露了他的网站的细节。他的博客类别、最近的帖子、搜索框和其他好东西都被隐藏起来,直到被一些忠实的 JavaScript 调用。

网站设计的另一个最新趋势是信息丰富的页脚。德里克·波瓦泽克在他 2005 年的重新设计中“拥抱了他的底部”(www.powazek.com/2005/09/000540.html),使他的网站的页脚非常突出和有用,而不是模糊不清和充满版权声明(见图 10-1 )。他有这个行业中最好的助手之一。它的设计目的和意图是激励、提供更多信息,并为访问者提供背景。

在这一章中,我们将实现一个将滑动导航和信息丰富的页脚结合到一个系统中的网站,该系统为当今许多网站上的传统“选项卡式”导航增加了细节和可用性增强。而且,正如您所料,它从一开始就将可访问性考虑在内。

The footer from Derek Powazek's blog. More than your average footer, Derek's is designed to be useful and provide the visitor with more context and blog-related functionality.

图 10.1。德里克·波瓦泽克博客的页脚。与普通的页脚相比,Derek 的页脚更有用,它为访问者提供了更多的上下文和与博客相关的功能。

辅助功能基础知识

网页可访问性最好被描述为一套指导原则或指南,帮助我们制作适合所有不同类型用户的网站,不管他们的能力如何。当我们编写网站代码时,我们努力将这些不同水平的能力考虑在内

  • 视力障碍(不同程度的失明、低视力和色盲)

  • 移动性或灵活性障碍(需要使用语音识别软件或硬件辅助工具来方便键盘使用或帮助补偿不同水平的精细运动控制)

  • 听觉障碍(例如,需要多媒体音频和视频的字幕和/或抄本)

  • 认知障碍(包括不同程度的阅读障碍、自闭症和其他学习障碍)

为了考虑到这种广泛的能力,我们需要做一些简单的事情。我们确保图像有适当的替代文本,以便视力受损的人有替代的表示,他们的屏幕阅读器软件可以读给他们听。我们实现了一些解决方案,允许用户调整页面上的文本大小,以便更容易阅读。我们在前景色和背景色之间有适当的颜色对比。我们标记表单域,使用结构化标记来确保屏幕上的元素使用最好的 HTML 元素,如标题、段落、列表、表格、表单按钮等等。

建设无障碍网站的一个长期问题是,作为开发人员、设计人员和内容创建者,我们几乎没有与残疾人一起工作的实践经验,以了解他们的真正需求以及我们如何避免为他们设置无障碍障碍。进入万维网联盟的网页可访问性倡议(www.w3.org/WAI)。

无障碍指南

Web Accessibility Initiative (WAI)致力于让每个人都能更方便地访问 Web,并制定了一套指导原则来帮助开发人员构建可访问的网站。不用担心;这不完全取决于我们。WAI 还为浏览器制造商和创作工具供应商制定了指导方针,以便他们能够确保人们使用的工具能够促进可访问性。这些其他的指导方针用户代理可访问性指导方针(UAAG)和创作工具可访问性指导方针(ATAG)是重要的,但是超出了我们作为开发人员所做的范围。我们应该坚定地把注意力放在网站内容无障碍指南上(WCAG)。

WCAG ( www.w3.org/WAI/intro/wcag.php)是一套指导方针,为我们创建无障碍网站提供了一些起点。然而,这并不意味着简单地遵循指南就能保证我们的网站是可访问的。为了创建可访问的网站,我们通常需要遵循指南中的原则,然后与使用辅助技术的人一起测试我们生成的网页。这确保了我们可以两全其美。我们通过遵守指导方针来实现技术合规性,并通过对残疾人的测试,我们创造了对他们实际有用的东西。

本章中的示例研究了 WCAG 的许多原理,包括用各种屏幕阅读器、语音识别软件和屏幕放大器进行测试的结果。

注意

要详细深入地了解可访问性,请阅读《Web 可访问性:Web 标准和法规遵从性》(编辑之友,ISBN: 1-59059-638-2)。

可访问性和 JavaScript

多年来,可访问性和 JavaScript 都是精通某个专业领域的专家的领域。这一领域的工作一直受到 web 开发人员代代相传的长期神话的阻碍。这个神话非常简单:为了确保你的网页是可访问的,它必须与 JavaScript 一起工作。

这种误解在很大程度上是由于 WCAG 检查站 6.3 ( www.w3.org/TR/WCAG10/wai-pageauth.html#tech-scripts)造成的,该检查站规定:

注意

当脚本、小程序或其他编程对象被关闭或不受支持时,确保页面可用。如果不可能,请在另一个可访问的页面上提供等效信息。

1999 年,当 WCAG 1.0 发布时,这是一个合理的指导方针。屏幕阅读器和浏览器远没有今天这么先进。人们普遍认为(并传播)屏幕阅读器不理解 JavaScript。开/关场景曾经相当准确。

今天的屏幕阅读器确实理解了今天的浏览器所支持的大部分 JavaScript,然而许多人仍然坚持认为这是不可能的。他们坚持 WCAG 检查点 6.3 所必需的假设:作为二进制开/关场景的可访问性和脚本。

进入网络可访问性和辅助技术的现代时代。是的,一些残障人士可能会使用无法处理脚本的浏览器,甚至关闭 JavaScript,但大多数人可能会使用支持脚本的常规浏览器。这不再是一个非黑即白的问题。确保一个页面在脚本打开或关闭的情况下工作更多的是关于互操作性,而不是 ?? 的可访问性。

因此,创建可访问的 JavaScript 的技巧不仅仅是确保我们的解决方案可以打开或关闭 JavaScript。我们还必须确保我们的解决方案与不同能力的用户和辅助技术兼容。在我们学习本章的其余部分时,请记住这一点。

无障碍解决方案

根据我们的经验,基于标准的 web 开发方法为残障人士提供了基本的可访问性以及与不同设备的基本互操作性:结构化 HTML、用于表示的 CSS 以及用于提供行为的最终 JavaScript 层。这种方法将在本章的例子中使用,以确保我们已经涵盖了基础知识。我们将使用语义 HTML,使用 CSS 进行设计,并交付一个可以打开或关闭 JavaScript 的解决方案。一旦我们完成了这些,我们将添加更多的脚本来确保系统对于使用各种辅助技术的人来说工作良好。

在我们开始编写代码之前,让我们先来看一下解决方案的简要概述以及实现这一切的文件(这些文件可以从本书的页面www.friendsofed.com下载):

  • 基本 HTML 文件,wscslide.html

  • 样式表,wscslide.css

  • JavaScript 函数,wscslide.js

图 10-2 显示了我们试图用滑动导航实现的效果的简单线框。当页面加载时,在任何 JavaScript 生效之前,导航窗格的初始位置在内容之后的底部。一旦最近文章的主导航链接被激活,我们使用一些基本的 JavaScript 来改变应用于导航窗格的 CSS,使其在视觉上位于导航下方。

一旦我们做到了这一点,我们将使用一些额外的 JavaScript 来设置该窗格的高度为 0,然后通过一步一步地改变该高度来创建一个滑动效果,将高度返回到其初始值。例如,如果最近的文章窗格的原始高度是 180 像素,我们将高度设置为 0,然后将其更改为 90 像素,然后 135 像素,然后 158 像素,依此类推,直到高度回到其原始大小 180 像素。

在最终的解决方案中,导航窗格的初始状态将如图 10-3 所示。

Wireframe of the sliding navigation functionality

图 10.2。滑动导航功能的线框

The initial state of the navigation, content, and additional navigational panes in their default positions

图 10.3。导航、内容和附加导航窗格在默认位置的初始状态

点击主链接后,导航窗格将滑动到最终位置,如图图 10-4 所示。

The "exposed" state, when one of the navigational panes is in its new position, visually just below the main navigation

图 10.4。“暴露”状态,其中一个导航窗格位于其新位置,视觉上位于主导航的正下方

它们代表了每个选项卡的两种“状态”:当信息被隐藏时和当信息被暴露时(当我们稍后检查解决方案的 CSS 和 JavaScript 部分时,这将是很重要的)。

从原始的 HTML 开始

为了使任何网站或 web 应用程序工作,我们需要一个通过干净的 HTML 提供的内容和功能的坚实基础。HTML 不仅应该在内容上有意义,而且应该在功能上有意义。尽管大多数人倾向于认为 HTML 只是内容,但它确实通过表单域和链接提供了基本的功能。在本章的后面,我们将使用这些链接作为脚本交互部分的基础。

对于这个解决方案,我们需要实现 HTML 的三个主要方面:

  • 航行区域

  • 主要内容

  • 页脚中的附加内容

导航将由一个无序列表组成,每个条目包含一个到相关页脚部分的链接。注意,这些hrefs只是指向页面的另一部分。

<ul id="nav">
<li><a href="#drop-posts">Recent Posts</a></li>
<li><a href="#drop-services">Services</a></li>
<li><a href="#drop-about">About Us</a></li>
</ul>

对于这个实现来说,主要内容基本上是不相关的。在这个例子中,它的主要目的是确保当访问者第一次到达页面时,页脚内容是不可见的。

页脚内容相当简单,包括几个部分。在本例中,我们使用了一个About部分、一个Recent Posts部分和一个Services部分。当详细查看该部分时,这些内容区域中的每一个都提供了对完整内容的一瞥,提供了各种摘要。我们将每个页脚部分包装在自己的<div></div>中,以便稍后提供适当的样式。

请注意,在本例中,Recent Posts窗格中链接的href是空白的,纯粹是为了方便。在文档中包含这样的空白链接会让所有用户感到困惑,尤其是那些有残疾的用户,尤其是那些使用屏幕阅读器软件的用户。

<div id="drops">
<div id="drop-posts">
  <h2>Recent posts:</h2>
  <ul>
    <li><a href="">Post 1</a></li>
    <li><a href="">Post 2</a></li>
    <li><a href="">Post 3</a></li>
    <li><a href="">Post 4</a></li>
    <li><a href="">Post 5</a></li>
    <li><a href="">Post 6</a></li>
  </ul>
</div>

<div id="drop-services">
  <h2>Services</h2>
  <p>things about services go here</p>
</div>

<div id="drop-about">
  <h2>About Us</h2>
  <p>Stuff about you goes here</p>
</div>

</div>  <!-- ends drops -->

每个divids在本例中只是为了清晰起见,并不一定是最佳选择。在包含所有附加导航窗格的div上放置一个dropsid意味着一个非常特殊的功能。如果我们以后改变这个网站,使它不再使用滑动导航,名称drops可能会令人困惑。最好给包装器div一个id来更好地表示它的功能,比如supplemental。然而,为了识别我们在这个例子中实现的行为,我们将保持iddrops

还要注意,我们在每个页脚部分添加了标题,为该部分提供了一种“标记”。为了举例,我们将这些标记为<h2>元素。您将需要决定<h2>是否适合您的使用,或者其他更低级的标题是否更合适。

这些代码块将作为我们其余实现的基础。我们将在添加样式和功能时对其进行修改。

添加演示文稿

这个例子的 CSS 相当简单,大部分都是为了修饰这个例子。我们添加一些样式来布局基本页面,指定颜色和背景图像,并格式化文本。有了这些,我们可以专注于页脚本身和我们需要的样式,让导航从顶部向下“滑动”。

最终完成 CSS 需要一些工作,但是核心布局保持不变。对于这个版本的滑动导航,我们使用绝对和相对定位将导航窗格从页面底部移动到顶部。正是这个动作形成了核心 CSS“开关”,使导航成为可能并保持其可访问性。主要机制只是一个链接,链接到包含我们想要的信息的页面的另一部分。当一个窗格被暴露时,它会占据页面顶部的位置,就像前面图 10-4 中的窗格一样。

使用 JavaScript 在 CSS 状态之间切换

在 JavaScript 因其在 Web 上的使用方式而遭到诋毁的时代,内联脚本、事件处理程序和样式属性的修改只是简单的做事方式。这些特别的变化通常与修改和定义飞行高度、宽度、背景颜色和各种其他样式属性有关。

注意

我们已经从那些黑暗的日子里取得了显著的进步,并努力将我们的 HTML 从我们的 CSS 和 JavaScript 中分离出来。现代技术努力确保我们的页面在打开和关闭时都能使用 JavaScript。这被称为“不引人注目的脚本”,是本书其他地方以及其他现代脚本书籍中的标准,如 Jeremy Keith 的书 DOM Scripting(ED 之友,ISBN: 1-59059-533-5)。

在我们目前的网络浏览器中,我们有一个更加可预测的工作环境。这种可预测性使我们能够改变高度和宽度之外的属性,并在包括 CSS 定位在内的预定义状态之间进行切换。将所有需要的 CSS 更改存储在一系列 CSS 规则中,我们可以简单地使用一些 JavaScript 来根据需要更改适当的包含元素的类。

出于滑动导航的目的,我们有两个核心状态:

  • 在初始页面加载时,我们的导航窗格出现在页面底部(见图 10-3 )。

  • 当一个导航项目被激活时(用键盘或鼠标),导航窗格移动到页面顶部(见图 10-4 )。

我们使用 JavaScript 将导航窗格的类更改为“exposed”,这使它在页面顶部导航下方有了合适的位置。我们创建一个样式规则,将任何窗格放置在正确的位置(来自样式表,wscslide.css):

/* apply to any div with class="exposed"
  that is in the <div id="drops"> */
div#drops div.exposed {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0; /* added to remove doubled-up padding when positioned */
}

请注意,这是可行的,因为我们将这些窗格定位在最近的相对定位的父窗格中,div#inner-wrap:

div#inner-wrap {
  background-color: #ccc;
  color: #000;
  position: relative;
  margin: 0;
  padding: 0.5em 0;
}

我们还使用 JavaScript 删除暴露的类,以将窗格返回到页面底部。

我们可以通过多种方式结合 JavaScript 来实现这一点。为了维护我们坚实的 HTML 基础,我们使用 JavaScript 拦截或“劫持”导航项目上的点击,并切换导航窗格的状态。它使用了一个名为toggle的函数,可以在暴露的导航窗格和原始状态之间切换。最好将这些动态地分配给每个导航项目,但是为了清楚起见,这最终导致我们的导航链接如下:

<a href="#drop-posts" onclick="return toggle('drop-posts');">
  Recent Posts
</a>

下面是来自wscslide.jstoggle函数的第一个版本:

function toggle(element) {

  var inner = document.getElementById('inner-wrap');

  /* if the scripting isn't supported, we want to return true so that
      default behavior of clicking the link works (i.e., take the user
      to the bottom part of the page)
  */

  if (!document.getElementById) return true;

  var elt = document.getElementById(element);

  // do a test on the className property on the element to
  // check for the exposed class

  if (/exposed/i.test(elt.className)) {
    // exposed state was found, so remove it

    elt.className = elt.className.replace(/exposed/g,'');
  } else {
    // add exposed to current class to respect any
    // styles/classes that already exist

    elt.className += " exposed";
  }

  return false;
}

注意

在这种情况下,核心功能由 hrefs 提供。确保它们到位意味着我们实现了与脚本的基本互操作性,无论是开还是关。不要把这和残疾人无障碍混淆。在解决了基本的互操作性需求之后,我们将在后面讨论可访问性。

这个脚本的关键部分是我们使用脚本来接管 HTML 中的<a href="#drop-posts">链接所提供的功能。这确保了在没有脚本支持的情况下使用浏览器的人将被带到页面的正确部分。

这是其余脚本的基础。有了这个主开关,我们就可以添加滑动行为了。

添加滑动行为

现在,我们已经使用脚本在每个导航窗格的暴露位置和正常位置之间切换,我们可以添加脚本来将导航从主导航的“下方”滑出。我们通过在激活exposed状态时将窗格的高度改为 0,并编写一个通过改变其高度来显示导航窗格的函数来实现这一点。

注意

作为替代,当我们激活暴露状态时,我们可以在 content div 上放置一个适当的 padding-top,以允许导航窗格位于内容之上。使用 CSStop 属性的负值,我们可以隐藏顶部导航下方的导航窗格,并使用 JavaScript 将 div#inner-wrap 向下滑动适当的距离。然而,在该解决方案的早期测试中,一些测试者报告了“起伏不定”的滑动行为。这在一定程度上是由于滑动了一个包含页面上所有内容的 div。随着页面的变大,滑动变得不顺畅。

这种解决方案和示例中使用的解决方案都需要额外的脚本,但是从技术角度来看,它们对可访问性都没有显著的影响。(虽然一般来说滑动导航可能会有问题,也可能没有问题,例如对于有认知障碍的人来说。)

完成的脚本包括几个使其工作良好的附加特性:

  • 简单的错误检查,防止用户在标签上点击两次。一旦导航窗格“移动”,该函数就返回,而不是再次调用它。

  • 一种基本的重置功能,可用于将选项卡重置为其原始状态。

  • 常数SLIDEINTERVAL,它让我们定义重复调用Reveal函数的速度。在这种情况下,我们每 65 毫秒调用一次。

我们可以对这个脚本进行一次大范围的浏览,一步一步地查看我们所做的更改。例如,我们可以详细研究动画策略。然而,我想把重点特别放在细节的可访问性方面,让大部分核心脚本自己说话。您可以在整个脚本中找到注释来详细说明发生了什么。下面是最终核心脚本的清单,wscslide.js:

var slideready = false;
var SLIDEINTERVAL = 65;
var revealTimer = null;
var moving  = false;

window.onload = function() {
  slideready = true;
}

function toggle(element) {
  if (!slideready || moving) return false;
  reset(element);
  if (!document.getElementById) return true;
  var elt = document.getElementById(element);
  var initialheight= elt.offsetHeight;
  Reveal(element, initialheight);
  //return false;
}
/* reset function used to set all navigation pane divs back to their
  initial state */

function reset(element) {
  var elt = document.getElementById('drops');
  var elts = elt.getElementsByTagName('div');
  var exposed = document.getElementById(element);
  for (i=0; i< elts.length; i++) {
    // we only want to reset divs that are acting as navigation panes
    // and exclude the current one that has been set to "exposed"
    if (!/drop-/i.test(elts[i].id) || (exposed ==  document.getElementById(elts[i].id))) {
      continue;
    }
    thiselt = elts[i];
    thiselt.className = thiselt.className.replace(/exposed/g,'')
    // set style back to overflow: hidden to remove visual artifacts
    // when switching from one tab to another
    thiselt.style.overflow = "hidden";

  }

  return;

}

function changeHeight(elt, dH) {
  var thiselt = document.getElementById(elt);
  // is this a reveal up or down? if up, the final target height is 0
  var targetHeight = (dH < 0) ? 0 : dH;

  // the current height of the element
  var currHeight = thiselt.offsetHeight;

  // the change in height required - to smooth the transition we reveal
  // half of the remaining height of the pane with each iteration
  var dHeight = Math.ceil((targetHeight - currHeight) / 2);

  newHeight = currHeight + dHeight;

  // if the difference is less than 1 pixel we'll stop moving,
  //clear the interval and set the height to the exact height
  // we started with
  if (Math.abs(dHeight) <= 1) {
    clearInterval(revealTimer);
    moving = false;
    newHeight = targetHeight;

  }

Adding sliding behaviors

// set the height to a new value
  thiselt.style.height =  newHeight + "px";

  // if the height is now zero, we need to remove the "exposed" state
  // set the height back to the original height and clear the JS set
  // value for height so that it is reset to the value found in the
  // original CSS
  if (thiselt.offsetHeight == 0) {
    thiselt.className = thiselt.className.replace(/exposed/g,'');
  //force a repaint for getting around Safari rendering issue
    thiselt.innerHTML = thiselt.innerHTML + '';
    thiselt.style.height = '';
  }
}

function Reveal(elt, dH) {
  // prevent the function from doing anything if it is already active
  if (moving) return;
  var thiselt = document.getElementById(elt);
  if (/exposed/i.test(thiselt.className)) {
    // if we are exposed, we want to slide the pane up instead of down
    dH = -dH;
// if we are sliding up, then we want to reset the overflow to hidden
    thiselt.style.overflow = "hidden";
  } else {
    // this opens the tab and respects classes that already exist
    thiselt.className += " exposed";
  }
  moving = true;
  // run the changeHeight function at the specified interval;
  // will run until we clear the interval
  revealTimer = setInterval("changeHeight('" + thiselt.id "','" + dH + "')", SLIDEINTERVAL);
}

可访问性在其中起了什么作用?

这个脚本的核心是可访问的,因为它是可互操作的。这给我们带来了以下好处:

  • 它可以在脚本打开或关闭的情况下工作。

  • 它使用 HTML 固有的核心行为,即指向同一页面上其他位置的简单链接。

  • (一般来说)我们只改变页面组件的 CSS,我们不使用display: none改变任何 CSS(这是众所周知的从屏幕阅读器的内部 DOM 中移除元素;见http://css-discuss.incutio.com/?page=ScreenreaderVisibility对这种经常令人沮丧的行为的讨论)。

  • 所有链接都允许键盘激活。我们没有为此做任何事情;这是固有的行为。在我们的脚本中,我们确保不做任何会剥夺键盘功能的事情。

这是一个好消息,但这并不意味着我们没有进一步的义务。为了确保这个解决方案确实可行,我们需要做一些额外的测试。让我们来看看其中的一些测试。

视力低下

CSS 的构造使得文本在所有浏览器中都是可伸缩的。导航窗格的高度从未被定义为静态数字;允许有弹性。每个导航窗格的高度是在脚本运行时确定的。这意味着,如果用户打开和关闭窗格,调整文本大小,然后再次打开和关闭窗格,脚本和 CSS 会自动调整以适应大小。我们成对地声明了背景色和前景色,这样当一个窗格被打开并且文本被调整大小时,它仍然是可读的,即使它从包含它的窗格中掉出。图 10-5 显示了一个调整了文本大小的页面。

Flexible sizing allows for the fonts and the navigational pane itself to be resized, without breaking the design.

图 10.5。灵活的大小调整允许在不破坏设计的情况下调整字体和导航窗格本身的大小。

注意

当使用屏幕放大镜时,我们注意到关闭链接没有出现在视野中。这并不比任何其他不使用缩放布局的网站更难使用。当然,可以做更多的实验来实现完全左对齐的缩放布局。

语音识别

这种特殊的解决方案似乎与语音识别软件配合得很好(Dragon NaturallySpeaking 用于测试),只有一个小的例外。语音识别用户能够专注于页面中的链接。他们可能会说“链接”,并显示有编号的选项,如图图 10-6 所示。

Using the link functionality in Dragon NaturallySpeaking to select the navigational links

图 10.6。使用 Dragon NaturallySpeaking 中的链接功能选择导航链接

如果使用该网站的人随后打开导航窗格,则在屏幕之外的链接将不能使用编号机制。然后用户可以再次说出“link”来重新填充链接数组,给出一个编号的链接选择,如图 10-7 所示。

When the navigational pane opens, the links contained within the pane are not available using the links array. The user would be required to say the command "link" again to repopulate the array, as seen here.

图 10.7。当导航窗格打开时,窗格中包含的链接无法使用 links 数组。用户需要再次说出命令“link”来重新填充阵列,如此处所示。

如果用户重复“链接”命令,然后关闭导航窗格(或切换到另一个窗格),他会在每个链接原来所在的地方看到一个可视的“工件”(见图 10-8 中的例子)。这可以通过手动重新填充链接数组来解决。

The visual artifacts of the links array. When using DOM scripting effects to move or generate content within the page, the links array does not automatically update. It requires the user to update it manually by giving the appropriate command, such as "link."

图 10.8。链接数组的视觉效果。当使用 DOM 脚本效果在页面中移动或生成内容时,links 数组不会自动更新。它要求用户通过给出适当的命令(如“link”)来手动更新它

屏幕阅读器

用屏幕阅读器测试这个解决方案揭示了一个问题。如果我们在劫持href s 时返回false(这是避免页面“跳转”到href指向的内部锚/ id的常见做法),那么我们将屏幕阅读器的“光标”留在它原来的位置。虽然我们在视觉上将相关的导航窗格移动到顶部,但我们没有提供对屏幕阅读器的相同访问。事实上,返回false会删除屏幕阅读器用户的功能。我们通过不允许页面跳转设置了一个障碍。

我们如何解决这个问题?这是我们必须再次检查可访问性和 JavaScript 的地方。如果我们仅仅停留在确保我们的页面在脚本打开或关闭时都可用,我们就不会意识到我们给屏幕阅读器用户带来的问题。重申一下,脚本开/关场景更多的是关于基本的互操作性,而不是关于残疾人的可访问性。

一种可能是不返回false,让历史堆栈顺其自然。在某些情况下,这样做甚至是可取的。然而,这意味着我们在页面中得到视觉上不和谐的“跳转”,并且最终隐藏了主导航,从视图中移除了切换点。这种解决方案不是特别有用,而且很可能会让用户感到厌烦,让签署该解决方案的人无法接受。

另一种可能是返回false,但是使用链接中的href并将焦点放在导航窗格上。如果我们这样做,我们将允许屏幕阅读器用户继续在正确的位置阅读,并确保我们不会覆盖我们的导航或跳转到整个屏幕。该解决方案在本章的最终文件中实现。

另一个屏幕阅读器问题是close链接。这可能会让屏幕阅读器用户感到困惑。在他们看来,他们只是简单地移动到页面的另一部分,那么他们为什么需要关闭任何东西呢?这个链接只有在视觉上看到页面的人才有意义。为了尝试解决这个问题,我们使用了一个关闭图标(典型的x图形),带有“返回主导航”的可选文本,以及一个合适的href。使用与主导航链接相同的策略,我们劫持href将屏幕阅读器光标放在下一个逻辑位置。

仅供键盘使用

只使用键盘的用户会遇到许多与屏幕阅读器用户相同的问题。特别是,将焦点放在导航窗格中对于允许键盘用户正确导航至关重要。

随着时间的推移,一种可能变得流行的解决方案是对tabindex属性使用无效值。将1tabindex值分配给一个元素允许该元素以编程方式接收焦点,但不会将其添加到文档的自然 tab 键顺序中。这个解决方案有点争议,因为无效的 valueby 规范,HTML 要求tabindex是 0 到 32767 之间的数字(详见www.w3.org/TR/REC-html40/interact/forms.html#adef-tabindex)。

tabindex解决方案是可接受的吗?IBM、Mozilla 和微软似乎都这么认为,尽管 IBM 的代表暗示他们知道这个解决方案不太正确(见www.csun.edu/cod/conf/2005/proceedings/2524.htm):

注意

请记住,这还不是任何 W3C 或其他官方标准的一部分。此时,有必要修改规则以获得完整的键盘可访问性。

一个无效的值可能会有不可预见的后果,并且在节点上操作 DOM 来包含一个tabindex值(在那里它不是一个有效的属性)似乎有点笨拙。我们实际上使用 JavaScript 只是为了掩盖这样一个事实,即我们正在做的事情根据 HTML 规范是无效的。

注意

1 tabindex 策略似乎得到了行业内的支持。web 超文本应用技术工作组(WHAT WG,www.whatwg.org),一个由 Web 浏览器供应商和其他感兴趣的团体组成的组织,正在致力于开发 HTML5。在该规范中,tabindex 是一个允许用于任何元素的属性,并且允许负的 tabindex 值。

结论

这是最终可行的解决方案吗?也许是,也许不是。我们所知道的是,我们已经尽了最大努力来确保我们在解决方案中有一个互操作性和可访问性的基线,具体做法是:

  • 通过简单的 HTML 代码提供内容和核心功能

  • 确保我们使用不引人注目的脚本

  • 确保我们的字体是可扩展的,并同时使用前景色和背景色

  • 以创造性的方式使用 CSS 来修改事物出现的方式,同时保持逻辑、语义结构

  • 使用 JavaScript,这样我们就可以利用链接和href来引导键盘用户或屏幕阅读器用户浏览页面

我们已经进行了基本测试,这使我们能够解决残疾人在使用该解决方案时将面临的许多问题。额外的测试将会告诉你这个解决方案对所有能力水平的用户是否有用。您必须对真实用户进行自己的可访问性测试。

在你的网站上试试这里展示的技术。本章已经给了你足够的信息来开始使用你自己的可访问的 JavaScript 解决方案。

第一部分:布局魔术

欢迎来到我们基于高级标准的网页设计之旅的第一部分。不出所料,第一部分涉及高级布局技术,因此重点主要放在 CSS 上。最终选择在这里收录(就像这本书的所有内容一样),是因为作者有一些有点有趣和不寻常的东西要说。面对现实吧,你在网上看到过多少关于 CSS 布局技术的教程?

作者们真的从帽子里拿出了一些特别的东西。Simon、Ethan 和 Andy 展示了在精益、语义标记的严格饮食下,通过巧妙使用类值、开箱即用定位等,实现设计精美的网站的不同方法。

Dan 向您展示了即使受到内容管理系统提供的内容的限制,您仍然可以创建外观精美的网站。他还加入了一些 Flash 图像替换和少量 DOM 脚本。

Jeff 专注于使用透明 png,以及 CSS 和 HTML,在您的站点上创建一些真正令人敬畏的设计效果。

作者谈到了他们应用时浏览器支持的不一致性,并解释了他们是如何解决这些问题的。你在这里看到的漂亮的布局是在没有借助很多黑客手段的情况下实现的!

旅途愉快。。。

第二部分:应用于 CSS 设计的有效打印技术

在旅程的下一部分,你将会看到如何将有效的印刷设计技术带入网页设计世界。无论你的背景如何,你都不得不承认这两种媒体之间有很多相似之处,印刷设计师学到的一些古老的经验对他们的数字兄弟来说非常有价值。

首先,马克专注于一个相当实验性的网页区域网格设计。他展示了网格在网站上使用时的强大功能。

然后,Rob 对印刷原则进行了有趣的研究。他展示了即使是一个简单的网站也可以通过在排版上花点心思来改变。

两位设计师都有印刷设计的背景,这一点显而易见。我希望有更多的他们这样的人能进入网络世界。

第三部分:DOM 脚本精粹

没有一些 DOM 脚本示例,哪本关于 web 标准的书是完整的?这就是为什么我把这本书的最后一部分致力于动态脚本技术来改善你的用户体验。

我特意尝试选择一些技术,这些技术对于设计性更强、开发性更差的人来说不会太可怕——不要害怕用 JavaScript 弄脏自己的手!

Ian 首先向我们展示了他拯救世界的技术:通过使用 DOM 脚本来指定只打印网页的重要部分,我们避免了浪费纸张和麻烦。

接下来,Cameron 演示了通过使用一些适当的脚本,在您的网页上为您的用户提供一个更加动态的交互环境并不困难。在他的案例研究过程中,他建立了一个动态的用户界面,无论用户使用什么样的分辨率/平台访问你的网站,该界面都会自动调整以获得最佳的浏览效果。他还展示了如何通过在不同的调色板中移动内容来创建用户可以定制的界面。

最后但同样重要的是,Derek 展示了如何实现一个有吸引力的滑动站点导航菜单,并保持其可访问性。我想在书中包含一些关于脚本和可访问性的内容,因为这是一个热门话题。Ajax 尤其会对页面的可访问性造成严重破坏,我们需要小心不要将残疾用户拒之门外。