Last Update: Mar.29,2024

JavaScript Sample

動作確認: ○Firefox99(Win7) ○Chrome100(Win7)

■ 入力欄のキーワード強調表示(highlight.js)

textarea 内で、入力とともに強調表示を行うカスタム要素(HIGH-LIGHT)を作成しました。まだ、日本語入力やスマホへの対応が不十分なので用途を限定した使い方しかできません。あらゆるケースに対応するにはまだまだ時間がかかりそうです。

キーワードの強調:

祇園精舎の鐘の声、諸行無常の響あり。沙羅双樹の花の色、盛者必衰の理をあらはす。おごれる人も久しからず、唯春の夜の夢のごとし。たけき者も遂にはほろびぬ、偏に風の前の塵に同じ。遠く異朝をとぶらへば、秦の趙高、漢の王莽、梁の周伊、唐の禄山、是等は皆旧主先皇の政にもしたがはず、楽みをきはめ、諫をも思ひいれず、天下の乱れむ事をさとらずして、民間の愁る所を知らざツしかば、久しからずして、亡じにし者どもなり。近く本朝をうかがふに、承平の将門、天慶の純友、康和の義親、平治の信頼、此等はおごれる心もたけき事も、皆とりどりにこそありしかども、まぢかくは六波羅の入道前太政大臣平朝臣清盛公と申しし人の有様、伝へ承るこそ、心も詞も及ばれね。

JavaScript の構文強調:

/* * Convert special HTML entities back to characters. * */ var htmlspecialchars_decode = function(s) { var c = {'gt':">",'lt':"<",'#039':"'",'quot':"\"",'amp':"&"}; return s.replace(/&(gt|lt|#039|quot|amp);/ig, function($0, $1) { return c[$1]; }); };

■ ソース

/*
 *  Highlight keywords.
 *
 *   s: input string
 *   k: array of keywords
 *   c: class name for css
 *
 */
var highlighting = function(s, k, c) {
  k.sort(function(a, b) {
    if (a < b) return 1;
    if (a > b) return -1;
    return 0;
  }).forEach(function(x) {
    s = s.replaceAll(x, `\x02${x}\x03`);
  });
  s = s.replace(/\x02/g, `<span style="${c}">`);
  s = s.replace(/\x03/g, `</span>`);
  return s;
};

jQuery(function($) {

  var k = [
    '趙高', '王莽', '周伊', '禄山', '将門',
    '純友', '義親', '信頼', '清盛'
  ];

  var d1 = document.querySelector('high-light#D1');
  d1.highlight = function(html) {
    return highlighting(html, k, 'background-color:yellow;color:red');
  };

  var d2 = document.querySelector('high-light#D2');
  d2.highlight = function(html) {
    const s = this.un_esc_html(html);
    const s1 = s.match(/^\s*/)[0];
    const s2 = s.match(/\s*$/)[0];
    const pre = new JSParser().parse(s);
    return s1 + $(pre).html() + s2;
  };

  $('form').on('submit', function() {
    $('input[name=T1]').val(d1.value);
    $('input[name=T2]').val(d2.value);
  });

});

■ ソース(highlight.js)

/*
 *  class HighLight
 *
 *  Date: 2024.3.29
 *
 *  Copyright (c) 2022, 2024 AOK (aokura.com)
 *
 */
class HighLight extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({
      delegatesFocus:true,
      mode:'open'
    });
    shadowRoot.innerHTML =
`<style>
:host {
  box-sizing:border-box;
  display:block;
  font-size:16px;
  padding:0;
  position:relative;
}
textarea {
  background-color:transparent;
  border:none;
  border-radius:3px;
  box-sizing:border-box;
  caret-color:black;
  color:transparent;
  font:inherit;
  line-height:1.2;
  margin:0;
  outline:2px solid #ccc;
  overflow:auto;
  padding:4px;
  position:absolute;
  resize:none;
  word-break:break-word;
}
textarea:focus {
  outline-color:blue;
}
div {
  scrollbar-color:transparent transparent;
}
</style>
<textarea spellcheck="false" part="textarea"></textarea>
<div></div>`;
    this._textarea = shadowRoot.querySelector('textarea');
    this._div = shadowRoot.querySelector('div');
  }
  get value() {
    return this._textarea.value;
  }
  set value(v) {
    this.textContent = v;
    this._textarea.value = v;
    this._input();
  }
  esc_html(text) {
    var div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
  un_esc_html(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    return div.textContent;
  }
  _get_margin() {
    var p = window.getComputedStyle(this);
    var t = window.getComputedStyle(this._textarea);
    var r = [0, 0];
    r[0] += parseInt(p.getPropertyValue('border-left-width'));
    r[1] += parseInt(p.getPropertyValue('border-top-width'));
    r[0] += parseInt(p.getPropertyValue('border-right-width'));
    r[1] += parseInt(p.getPropertyValue('border-bottom-width'));
    r[0] += parseInt(p.getPropertyValue('padding-left'));
    r[1] += parseInt(p.getPropertyValue('padding-top'));
    r[0] += parseInt(p.getPropertyValue('padding-right'));
    r[1] += parseInt(p.getPropertyValue('padding-bottom'));
    r[0] += parseInt(t.getPropertyValue('margin-left'));
    r[1] += parseInt(t.getPropertyValue('margin-top'));
    r[0] += parseInt(t.getPropertyValue('margin-right'));
    r[1] += parseInt(t.getPropertyValue('margin-bottom'));
    return r;
  }
  _outer_resize() {
    var r = this._get_margin();
    var w = this.getBoundingClientRect().width;
    var h = this.getBoundingClientRect().height;
    if (w == 0) {
      w = this._textarea.getBoundingClientRect().width;
      this.style.setProperty('width', `${w+r[0]}px`);
    } else {
      this._textarea.style.setProperty('width', `${w-r[0]}px`);
      this._div.style.setProperty('width', `${w-r[0]}px`);
    }
    if (h == 0) {
      h = this._textarea.getBoundingClientRect().height;
      this.style.setProperty('height', `${h+r[1]}px`);
    } else {
      this._textarea.style.setProperty('height', `${h-r[1]}px`);
      this._div.style.setProperty('height', `${h-r[1]}px`);
    }
  }
  _inner_resize() {
    var r = this._get_margin();
    var w = this._textarea.getBoundingClientRect().width;
    var h = this._textarea.getBoundingClientRect().height;
    var t = window.getComputedStyle(this._textarea);
    var v = t.getPropertyValue('resize');
    if (v == 'horizontal' || v == 'both') {
      this.style.setProperty('width', `${w+r[0]}px`);
      this._div.style.setProperty('width', `${w}px`);
    }
    if (v == 'vertical' || v == 'both' ||
        this.getAttribute('expand') == 'true') {
      this.style.setProperty('height', `${h+r[1]}px`);
      this._div.style.setProperty('height', `${h}px`);
    }
  }
  _expand_height() {
    if (this.getAttribute('expand') == 'true') {
      var style = window.getComputedStyle(this._div);
      this._textarea.style.setProperty('overflow-y', 'hidden');
      this._div.style.setProperty('height', 'auto');
      if (this._div.innerHTML == '') {
        this._div.innerHTML = '&nbsp;';
      }
      var h = style.getPropertyValue('height');
      if (this._pre_height != h) {
        this._textarea.style.setProperty('height', h);
        this._pre_height = h;
      }
      this._div.style.setProperty('height', h);
    }
  }
  _input() {
    var s = this.esc_html(this._textarea.value);
    if (/\n$/.test(s)) s += ' ';
    if (this.highlight) {
      this._div.innerHTML = this.highlight(s);
    } else {
      this._div.innerHTML = s;
    }
    this._expand_height();
  }
  _copy_css() {
    var t = window.getComputedStyle(this._textarea);
    const css_names = [
      'border', 'box-sizing', 'font', 'letter-spacing',
      'line-height', 'margin', 'overflow', 'padding',
      'text-decoration', 'white-space', 'word-break',
      'word-spacing','vertical-align'
    ];
    css_names.forEach(function(s) {
      this.style.setProperty(s, t.getPropertyValue(s));
    }, this._div);
  }
  connectedCallback() {

    var a = this;

    document.addEventListener('DOMContentLoaded', function() {
      a._copy_css();
      a._outer_resize();
      a._textarea.value = a.textContent;
      a._input();
    });

    window.addEventListener('resize', function() {
      a._outer_resize();
      a._expand_height();
    });

    a._textarea.addEventListener('scroll', function() {
      a._div.scrollTop = a._textarea.scrollTop;
      a._div.scrollLeft = a._textarea.scrollLeft;
    });

    a._textarea.addEventListener('input', function() {
      a._input();
    });

    const observer = new MutationObserver(function() {
      a._copy_css();
      a._inner_resize();
    });

    observer.observe(this._textarea, {
      attriblutes: true,
      attributeFilter: ['style']
    });

    a._textarea.addEventListener('compositionstart', function() {
      a._textarea.style.setProperty('color', 'inherit');
      a._div.style.setProperty('visibility', 'hidden');
    });

    a._textarea.addEventListener('compositionend', function() {
      a._textarea.style.setProperty('color', 'transparent');
      a._div.style.setProperty('visibility', 'visible');
    });

  }
}

customElements.define('high-light', HighLight);