Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

前端でよく使われるデザインパターン

抽象行動(behavior)パターン#

画像プレビュー#

私たちのタスクは、固定リスト内の画像要素に「プレビュー」機能を追加することです。オンラインデモ
対応する HTML ページは以下の通りです:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>画像プレビュー</title>
  <style>
    #list {
      list-style-type: none;
      justify-content: flex-start;
      display: flex;
      flex-wrap: wrap;
    }

    #list li {
      padding: 10px;
      margin: 0;
    }
    #list img {
      height: 200px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <ul id="list">
    <li>
      <img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
    </li>
    <li>
      <img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
    </li>
    <li>
      <img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
    </li>
    <li>
      <img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
    </li>
    <li>
      <img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
    </li>
  </ul>
</body>
</html>
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    preview(list, target);
  }
});
list.addEventListener('preview', ({detail}) => {
  detail.showMask();
});
function useBehavior(context) {
  const {type, getDetail} = context;
  return function (subject, target) {
    const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
    target.dispatchEvent(event);
  };
}
const preview = useBehavior({
  type: 'preview',

  /*
    @subject: <ul>要素
    @target: 選択された画像要素
  */
  getDetail(subject, target) {
    const imgs = Array.from(subject.querySelectorAll('img'));
    const selected = imgs.indexOf(target); // 選択された画像のインデックスを取得
    let mask = document.getElementById('mask');

    // maskが存在しない場合、mask要素を作成
    if(!mask) {
      mask = document.createElement('div');
      mask.id = 'mask';
      mask.innerHTML = `
        <a class="previous" href="###">&lt;</a>
        <img src="${imgs[selected].src}">
        <a class="next" href="###">&gt;</a>    
      `;
      // #mask要素にスタイルを設定:
      Object.assign(mask.style, {
        position: 'absolute',
        left: 0,
        top: 0,
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(0,0,0,0.8)',
        display: 'none',
        alignItems: 'center',
        justifyContent: 'space-between',
      });

      // #mask要素の左右の<a>要素にスタイルを設定:
      mask.querySelectorAll('a').forEach((a) => {
        Object.assign(a.style, {
          width: '30px',
          textAlign: 'center',
          fontSize: '2rem',
          color: '#fff',
          textDecoration: 'none',
        });
      });
      document.body.appendChild(mask);

      // #mask要素にクリックイベント処理関数を追加:
      let idx = selected;
      mask.addEventListener('click', (evt) => {
        const target = evt.target;
        if(target === mask) { // クリックした対象がmask要素の場合、mask要素を非表示にする
          mask.style.display = 'none';
        } else if(target.className === 'previous') { // 前の画像を表示
          update(--idx);
        } else if(target.className === 'next') { // 次の画像を表示
          update(++idx);
        }
      });
    }

    // img要素のsrc属性を指定された画像に設定
    function update(idx) {
      const [previous, next] = [...mask.querySelectorAll('a')];
      previous.style.visibility = idx ? 'visible' : 'hidden';
      next.style.visibility = idx < imgs.length - 1 ? 'visible' : 'hidden';
      const img = mask.querySelector('img');
      img.src = imgs[idx].src;
    }

    return {
      showMask() { // 選択された画像のプレビューを表示
        mask.style.display = 'flex';
        update(selected);
      },
    };
  },
});

画像セレクター#

オンラインデモ

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>抽象行動</title>
  <style>
    #list {
      list-style-type: none;
      justify-content: flex-start;
      display: flex;
      flex-wrap: wrap;
    }

    #list li {
      padding: 10px;
      margin: 0;
    }
    #list img {
      height: 200px;
      cursor: pointer;
      box-sizing: border-box;
      padding: 5px;
    }

    #list img.selected {
      border: solid 5px #37c;
      padding: 0;
    }
  </style>
</head>
<body>
  <ul id="list">
    <li>
      <img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
    </li>
    <li>
      <img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
    </li>
    <li>
      <img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
    </li>
    <li>
      <img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
    </li>
    <li>
      <img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
    </li>
  </ul>
</body>
</html>
function useBehavior(context) {
  const {type, getDetail} = context;
  return function(subject, target) {
    const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
    target.dispatchEvent(event);
  }
}

const select = useBehavior({
  type: 'select',
  data: {
    picked: new Set(), // 選択された画像の集合
  },
  getDetail(subject, target) {
    const picked = this.data.picked;

    if(picked.has(target)) {
      target.className = '';
      picked.delete(target);
    } else {
      target.className = 'selected';
      picked.add(target);
    }

    return {
      changed: target,
      picked,
    };
  },
});
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    select(list, target);
  }
});

list.addEventListener('select', ({detail}) => {
  // 何もしない
  console.log(detail.changed, detail.picked);
});

最初のストーリーと 2 番目のストーリーを通じて、「プレビュー」と「選択」という 2 つの行動を組み合わせることができます。たとえば、マウスをクリックすると同時に alt キーを押すと、画像を選択することを意味します。それ以外の場合は画像をプレビューします:

/* ...省略された他のコード... */

const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    if(evt.altKey) {
      select(list, target);
    } else {
      preview(list, target);
    }
  }
});

list.addEventListener('preview', ({detail}) => {
  const {showMask} = detail;
  showMask();
});

上記のコードのように、抽象行動のパターンは、コンポーネントが複数の行動を柔軟に組み合わせたり、アンロードしたりできることを許可し、互いに衝突しないようにします。

ミディエーター(Mediator)パターン#

次の新しいタスクは、同期スクロールの編集とプレビューエリアを実装することです。これは、いくつかのオンライン編集型 Web アプリケーションで一般的なインタラクション形式です。オンラインデモ

<body>
  <textarea id="editor" oninput="this.editor.update()"
            rows="6" cols="60">
2001年から2003年の間に、ジュディス・ミラーはニューヨーク・タイムズに一連の記事を発表し、イラクが大量破壊兵器を生産する能力と野心を持っていると主張しました。これはフェイクニュースです。

当時を振り返ると、ミラーが書いたこれらの物語が2013年にアメリカがイラク戦争を開始する決定にどのような役割を果たしたのかは不明です;ミラーと同じ情報源からの情報は、ブッシュ政権の対外政策チームと大きな関連があります。しかし、ニューヨーク・タイムズはこの政策を支持する役割を果たしました。特に民主党員にとっては、彼らは本来ブッシュの政策に対してより強く反対すべきでした。結局のところ、ニューヨーク・タイムズは無視される地方の小さな新聞ではなく、アメリカ全体で最も影響力のある新聞であり、一般的に左寄りの傾向があると見なされています。ミラーの物語は、ある意味で新聞の政治的傾向に一致しています。

私たちはミラーの誤りと最近のFacebookのフェイクニュース問題を関連付けて見ることができます;Facebookは自らの物語で「フェイクニュースは悪い」と私たちに告げています。しかし、私は異なる見解を持っています:**ニュースが真実かどうかはそれほど重要ではなく、何がニュースであるかを決定するのは最も重要です**。

<!--more-->

#### Facebookのメディア商業化

[集約理論](https://stratechery.com/2015/aggregation-theory/)では、配分に基づく経済権の消失が強力な仲介者の台頭を引き起こし、彼らが顧客体験を掌握し、供給者の商品化を行うことを説明しました。[Facebookの例では](https://stratechery.com/2016/the-fang-playbook/)、ソーシャルネットワークが台頭したのは、以前のオフラインの社会ネットワークがオンラインネットワークに移行しているからです。人間の本質が社会的であることを考えると、ユーザーはFacebookでニュースを読み、意見を発表し、情報を得るために時間を費やし始めました。

...(この部分は省略されています)
                     
  </textarea>
  <div id="preview"> </div>
  <div id="hintbar"> 0% </div>
</body>
body{
  display: flex;
}

#editor {
  width: 45%;
  height: 350px;
  margin-right: 10px;
}

#preview {
  width: 45%;
  height: 350px;
  overflow: scroll;
}

#hintbar {
  position: absolute;
  right: 10px;
}
function Editor(input, preview) {
  this.update = function () {
    preview.innerHTML = markdown.toHTML(input.value);
  };
  input.editor = this;
  this.update();
}
new Editor(editor, preview);

class PubSub {
  constructor() {
    this.subscribers = {};
  }
  pub(type, sender, data){
    var subscribers = this.subscribers[type];
    subscribers.forEach(function(subscriber){
      subscriber({type, sender, data});
    });
  }
  sub(type, receiver, fn){
    this.subscribers[type] = this.subscribers[type] || [];
    this.subscribers[type].push(fn.bind(receiver));
  }
}

function scrollTo({data:p}){
  this.scrollTop = p * (this.scrollHeight - this.clientHeight);
}

var mediator = new PubSub();
mediator.sub('scroll', preview, scrollTo);
mediator.sub('scroll', editor, scrollTo);
mediator.sub('scroll', hintbar, function({data:p}){
  this.innerHTML = Math.round(p * 100) + '%';
});

function debounce(fn, ms = 100) {
  let debounceTimer = null;
  return function(...args) {
    if(debounceTimer) clearTimeout(debounceTimer);

    debounceTimer = setTimeout(() => {
      fn.apply(this, args);
    }, ms);
  }
}

let scrollingTarget = null;
editor.addEventListener('scroll', debounce(function(evt){
  scrollingTarget = null;
}));

function updateScroll(evt) {
  var target = evt.target;
  if(!scrollingTarget) scrollingTarget = target;
  if(scrollingTarget === target) {
    var scrollRange = target.scrollHeight - target.clientHeight,
      p = target.scrollTop / scrollRange;
    mediator.pub('scroll', target, p);
  }
}
editor.addEventListener('scroll', updateScroll);
preview.addEventListener('scroll', updateScroll);
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。